Merge pull request #1621 from ArthurHoaro/feature/tag-separators
This commit is contained in:
commit
d9d71b10c3
44 changed files with 657 additions and 171 deletions
|
@ -60,11 +60,13 @@ class Bookmark
|
|||
/**
|
||||
* Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $data
|
||||
* @param string $tagsSeparator Tags separator loaded from the config file.
|
||||
* This is a context data, and it should *never* be stored in the Bookmark object.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function fromArray(array $data): Bookmark
|
||||
public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark
|
||||
{
|
||||
$this->id = $data['id'] ?? null;
|
||||
$this->shortUrl = $data['shorturl'] ?? null;
|
||||
|
@ -77,7 +79,7 @@ public function fromArray(array $data): Bookmark
|
|||
if (is_array($data['tags'])) {
|
||||
$this->tags = $data['tags'];
|
||||
} else {
|
||||
$this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
|
||||
$this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator);
|
||||
}
|
||||
if (! empty($data['updated'])) {
|
||||
$this->updated = $data['updated'];
|
||||
|
@ -348,7 +350,12 @@ public function getTags(): array
|
|||
*/
|
||||
public function setTags(?array $tags): Bookmark
|
||||
{
|
||||
$this->setTagsString(implode(' ', $tags ?? []));
|
||||
$this->tags = array_map(
|
||||
function (string $tag): string {
|
||||
return $tag[0] === '-' ? substr($tag, 1) : $tag;
|
||||
},
|
||||
tags_filter($tags, ' ')
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -420,11 +427,13 @@ public function setSticky(?bool $sticky): Bookmark
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string Bookmark's tags as a string, separated by a space
|
||||
* @param string $separator Tags separator loaded from the config file.
|
||||
*
|
||||
* @return string Bookmark's tags as a string, separated by a separator
|
||||
*/
|
||||
public function getTagsString(): string
|
||||
public function getTagsString(string $separator = ' '): string
|
||||
{
|
||||
return implode(' ', $this->getTags());
|
||||
return tags_array2str($this->getTags(), $separator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -444,19 +453,13 @@ public function isNote(): bool
|
|||
* - trailing dash in tags will be removed
|
||||
*
|
||||
* @param string|null $tags
|
||||
* @param string $separator Tags separator loaded from the config file.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTagsString(?string $tags): Bookmark
|
||||
public function setTagsString(?string $tags, string $separator = ' '): Bookmark
|
||||
{
|
||||
// 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;
|
||||
$this->setTags(tags_str2array($tags, $separator));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
@ -507,7 +510,7 @@ public function getAdditionalContentEntry(string $key, $default = null)
|
|||
*/
|
||||
public function renameTag(string $fromTag, string $toTag): void
|
||||
{
|
||||
if (($pos = array_search($fromTag, $this->tags)) !== false) {
|
||||
if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
|
||||
$this->tags[$pos] = trim($toTag);
|
||||
}
|
||||
}
|
||||
|
@ -519,7 +522,7 @@ public function renameTag(string $fromTag, string $toTag): void
|
|||
*/
|
||||
public function deleteTag(string $tag): void
|
||||
{
|
||||
if (($pos = array_search($tag, $this->tags)) !== false) {
|
||||
if (($pos = array_search($tag, $this->tags ?? [])) !== false) {
|
||||
unset($this->tags[$pos]);
|
||||
$this->tags = array_values($this->tags);
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ public function __construct(ConfigManager $conf, History $history, Mutex $mutex,
|
|||
}
|
||||
}
|
||||
|
||||
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
|
||||
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use Exception;
|
||||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
|
||||
/**
|
||||
* Class LinkFilter.
|
||||
|
@ -58,12 +59,16 @@ class BookmarkFilter
|
|||
*/
|
||||
private $bookmarks;
|
||||
|
||||
/** @var ConfigManager */
|
||||
protected $conf;
|
||||
|
||||
/**
|
||||
* @param Bookmark[] $bookmarks initialization.
|
||||
*/
|
||||
public function __construct($bookmarks)
|
||||
public function __construct($bookmarks, ConfigManager $conf)
|
||||
{
|
||||
$this->bookmarks = $bookmarks;
|
||||
$this->conf = $conf;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,10 +112,14 @@ public function filter(
|
|||
$filtered = $this->bookmarks;
|
||||
}
|
||||
if (!empty($request[0])) {
|
||||
$filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
|
||||
$filtered = (new BookmarkFilter($filtered, $this->conf))
|
||||
->filterTags($request[0], $casesensitive, $visibility)
|
||||
;
|
||||
}
|
||||
if (!empty($request[1])) {
|
||||
$filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility);
|
||||
$filtered = (new BookmarkFilter($filtered, $this->conf))
|
||||
->filterFulltext($request[1], $visibility)
|
||||
;
|
||||
}
|
||||
return $filtered;
|
||||
case self::$FILTER_TEXT:
|
||||
|
@ -280,8 +289,9 @@ private function filterFulltext(string $searchterms, string $visibility = 'all')
|
|||
*
|
||||
* @return string generated regex fragment
|
||||
*/
|
||||
private static function tag2regex(string $tag): string
|
||||
protected function tag2regex(string $tag): string
|
||||
{
|
||||
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
|
||||
$len = strlen($tag);
|
||||
if (!$len || $tag === "-" || $tag === "*") {
|
||||
// nothing to search, return empty regex
|
||||
|
@ -295,12 +305,13 @@ private static function tag2regex(string $tag): string
|
|||
$i = 0; // start at first character
|
||||
$regex = '(?='; // use positive lookahead
|
||||
}
|
||||
$regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
|
||||
// before tag may only be the separator or the beginning
|
||||
$regex .= '.*(?:^|' . $tagsSeparator . ')';
|
||||
// iterate over string, separating it into placeholder and content
|
||||
for (; $i < $len; $i++) {
|
||||
if ($tag[$i] === '*') {
|
||||
// placeholder found
|
||||
$regex .= '[^ ]*?';
|
||||
$regex .= '[^' . $tagsSeparator . ']*?';
|
||||
} else {
|
||||
// regular characters
|
||||
$offset = strpos($tag, '*', $i);
|
||||
|
@ -316,7 +327,8 @@ private static function tag2regex(string $tag): string
|
|||
$i = $offset;
|
||||
}
|
||||
}
|
||||
$regex .= '(?:$| ))'; // after the tag may only be a space or the end
|
||||
// after the tag may only be the separator or the end
|
||||
$regex .= '(?:$|' . $tagsSeparator . '))';
|
||||
return $regex;
|
||||
}
|
||||
|
||||
|
@ -334,14 +346,15 @@ private static function tag2regex(string $tag): string
|
|||
*/
|
||||
public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
|
||||
{
|
||||
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
|
||||
// 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);
|
||||
$inputTags = tags_str2array($inputTags, $tagsSeparator);
|
||||
}
|
||||
|
||||
if (!count($inputTags)) {
|
||||
if (count($inputTags) === 0) {
|
||||
// no input tags
|
||||
return $this->noFilter($visibility);
|
||||
}
|
||||
|
@ -358,7 +371,7 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
|
|||
}
|
||||
|
||||
// build regex from all tags
|
||||
$re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
|
||||
$re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/';
|
||||
if (!$casesensitive) {
|
||||
// make regex case insensitive
|
||||
$re .= 'i';
|
||||
|
@ -378,7 +391,8 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
|
|||
continue;
|
||||
}
|
||||
}
|
||||
$search = $link->getTagsString(); // build search string, start with tags of current link
|
||||
// build search string, start with tags of current link
|
||||
$search = $link->getTagsString($tagsSeparator);
|
||||
if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
|
||||
// description given and at least one possible tag found
|
||||
$descTags = array();
|
||||
|
@ -390,9 +404,9 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
|
|||
);
|
||||
if (count($descTags[1])) {
|
||||
// there were some tags in the description, add them to the search string
|
||||
$search .= ' ' . implode(' ', $descTags[1]);
|
||||
$search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator);
|
||||
}
|
||||
};
|
||||
}
|
||||
// match regular expression with search string
|
||||
if (!preg_match($re, $search)) {
|
||||
// this entry does _not_ match our regex
|
||||
|
@ -422,7 +436,7 @@ public function filterUntagged(string $visibility)
|
|||
}
|
||||
}
|
||||
|
||||
if (empty(trim($link->getTagsString()))) {
|
||||
if (empty($link->getTags())) {
|
||||
$filtered[$key] = $link;
|
||||
}
|
||||
}
|
||||
|
@ -537,10 +551,11 @@ protected function postProcessFoundPositions(array $fieldLengths, array $foundPo
|
|||
*/
|
||||
protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
|
||||
{
|
||||
$tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' '));
|
||||
$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') .'\\';
|
||||
$content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') .'\\';
|
||||
|
||||
$lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
|
||||
$nextField = $lengths['title']['end'] + 1;
|
||||
|
@ -548,7 +563,7 @@ protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths):
|
|||
$nextField = $lengths['description']['end'] + 1;
|
||||
$lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
|
||||
$nextField = $lengths['url']['end'] + 1;
|
||||
$lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())];
|
||||
$lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)];
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
|
|
@ -176,3 +176,49 @@ function is_note($linkUrl)
|
|||
{
|
||||
return isset($linkUrl[0]) && $linkUrl[0] === '?';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an array of tags from a given tag string, with provided separator.
|
||||
*
|
||||
* @param string|null $tags String containing a list of tags separated by $separator.
|
||||
* @param string $separator Shaarli's default: ' ' (whitespace)
|
||||
*
|
||||
* @return array List of tags
|
||||
*/
|
||||
function tags_str2array(?string $tags, string $separator): array
|
||||
{
|
||||
// For whitespaces, we use the special \s regex character
|
||||
$separator = $separator === ' ' ? '\s' : $separator;
|
||||
|
||||
return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a tag string with provided separator from a list of tags.
|
||||
* Note that given array is clean up by tags_filter().
|
||||
*
|
||||
* @param array|null $tags List of tags
|
||||
* @param string $separator
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function tags_array2str(?array $tags, string $separator): string
|
||||
{
|
||||
return implode($separator, tags_filter($tags, $separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean an array of tags: trim + remove empty entries
|
||||
*
|
||||
* @param array|null $tags List of tags
|
||||
* @param string $separator
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function tags_filter(?array $tags, string $separator): array
|
||||
{
|
||||
$trimDefault = " \t\n\r\0\x0B";
|
||||
return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string {
|
||||
return trim($entry, $trimDefault . $separator);
|
||||
}, $tags ?? [])));
|
||||
}
|
||||
|
|
|
@ -368,6 +368,7 @@ protected function setDefaultValues()
|
|||
$this->setEmpty('general.default_note_title', 'Note: ');
|
||||
$this->setEmpty('general.retrieve_description', true);
|
||||
$this->setEmpty('general.enable_async_metadata', true);
|
||||
$this->setEmpty('general.tags_separator', ' ');
|
||||
|
||||
$this->setEmpty('updates.check_updates', false);
|
||||
$this->setEmpty('updates.check_updates_branch', 'stable');
|
||||
|
|
|
@ -68,15 +68,16 @@ protected function formatTagList($bookmark)
|
|||
*/
|
||||
protected function formatTagListHtml($bookmark)
|
||||
{
|
||||
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
|
||||
if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
|
||||
return $this->formatTagList($bookmark);
|
||||
}
|
||||
|
||||
$tags = $this->tokenizeSearchHighlightField(
|
||||
$bookmark->getTagsString(),
|
||||
$bookmark->getTagsString($tagsSeparator),
|
||||
$bookmark->getAdditionalContentEntry('search_highlight')['tags']
|
||||
);
|
||||
$tags = $this->filterTagList(explode(' ', $tags));
|
||||
$tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator));
|
||||
$tags = escape($tags);
|
||||
$tags = $this->replaceTokensArray($tags);
|
||||
|
||||
|
@ -88,7 +89,7 @@ protected function formatTagListHtml($bookmark)
|
|||
*/
|
||||
protected function formatTagString($bookmark)
|
||||
{
|
||||
return implode(' ', $this->formatTagList($bookmark));
|
||||
return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -267,7 +267,7 @@ protected function formatTagListHtml($bookmark)
|
|||
*/
|
||||
protected function formatTagString($bookmark)
|
||||
{
|
||||
return implode(' ', $this->formatTagList($bookmark));
|
||||
return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -351,6 +351,7 @@ protected function formatUpdatedTimestamp(Bookmark $bookmark)
|
|||
|
||||
/**
|
||||
* Format tag list, e.g. remove private tags if the user is not logged in.
|
||||
* TODO: this method is called multiple time to format tags, the result should be cached.
|
||||
*
|
||||
* @param array $tags
|
||||
*
|
||||
|
|
|
@ -24,6 +24,12 @@ public function index(Request $request, Response $response): Response
|
|||
$fromTag = $request->getParam('fromtag') ?? '';
|
||||
|
||||
$this->assignView('fromtag', escape($fromTag));
|
||||
$separator = escape($this->container->conf->get('general.tags_separator', ' '));
|
||||
if ($separator === ' ') {
|
||||
$separator = ' ';
|
||||
$this->assignView('tags_separator_desc', t('whitespace'));
|
||||
}
|
||||
$this->assignView('tags_separator', $separator);
|
||||
$this->assignView(
|
||||
'pagetitle',
|
||||
t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
||||
|
@ -85,4 +91,31 @@ public function save(Request $request, Response $response): Response
|
|||
|
||||
return $this->redirect($response, $redirect);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /admin/tags/change-separator - Change tag separator
|
||||
*/
|
||||
public function changeSeparator(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkToken($request);
|
||||
|
||||
$reservedCharacters = ['-', '.', '*'];
|
||||
$newSeparator = $request->getParam('separator');
|
||||
if ($newSeparator === null || mb_strlen($newSeparator) !== 1) {
|
||||
$this->saveErrorMessage(t('Tags separator must be a single character.'));
|
||||
} elseif (in_array($newSeparator, $reservedCharacters, true)) {
|
||||
$reservedCharacters = implode(' ', array_map(function (string $character) {
|
||||
return '<code>' . $character . '</code>';
|
||||
}, $reservedCharacters));
|
||||
$this->saveErrorMessage(
|
||||
t('These characters are reserved and can\'t be used as tags separator: ') . $reservedCharacters
|
||||
);
|
||||
} else {
|
||||
$this->container->conf->set('general.tags_separator', $newSeparator, true, true);
|
||||
|
||||
$this->saveSuccessMessage('Your tags separator setting has been updated!');
|
||||
}
|
||||
|
||||
return $this->redirect($response, '/admin/tags');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ public function changeVisibility(Request $request, Response $response): Response
|
|||
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
||||
$data = $formatter->format($bookmark);
|
||||
$this->executePageHooks('save_link', $data);
|
||||
$bookmark->fromArray($data);
|
||||
$bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
|
||||
|
||||
$this->container->bookmarkService->set($bookmark, false);
|
||||
++$count;
|
||||
|
@ -167,7 +167,7 @@ public function pinBookmark(Request $request, Response $response, array $args):
|
|||
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
||||
$data = $formatter->format($bookmark);
|
||||
$this->executePageHooks('save_link', $data);
|
||||
$bookmark->fromArray($data);
|
||||
$bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
|
||||
|
||||
$this->container->bookmarkService->set($bookmark);
|
||||
|
||||
|
|
|
@ -113,7 +113,10 @@ public function save(Request $request, Response $response): Response
|
|||
$bookmark->setDescription($request->getParam('lf_description'));
|
||||
$bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
|
||||
$bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
|
||||
$bookmark->setTagsString($request->getParam('lf_tags'));
|
||||
$bookmark->setTagsString(
|
||||
$request->getParam('lf_tags'),
|
||||
$this->container->conf->get('general.tags_separator', ' ')
|
||||
);
|
||||
|
||||
if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
|
||||
&& true !== $this->container->conf->get('general.enable_async_metadata', true)
|
||||
|
@ -128,7 +131,7 @@ public function save(Request $request, Response $response): Response
|
|||
$data = $formatter->format($bookmark);
|
||||
$this->executePageHooks('save_link', $data);
|
||||
|
||||
$bookmark->fromArray($data);
|
||||
$bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
|
||||
$this->container->bookmarkService->set($bookmark);
|
||||
|
||||
// If we are called from the bookmarklet, we must close the popup:
|
||||
|
@ -221,6 +224,11 @@ protected function buildLinkDataFromUrl(Request $request, string $url): array
|
|||
|
||||
protected function buildFormData(array $link, bool $isNew, Request $request): array
|
||||
{
|
||||
$link['tags'] = strlen($link['tags']) > 0
|
||||
? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ')
|
||||
: $link['tags']
|
||||
;
|
||||
|
||||
return escape([
|
||||
'link' => $link,
|
||||
'link_is_new' => $isNew,
|
||||
|
|
|
@ -95,6 +95,10 @@ public function index(Request $request, Response $response): Response
|
|||
$next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
|
||||
}
|
||||
|
||||
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||
$searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator));
|
||||
$searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
|
||||
|
||||
// Fill all template fields.
|
||||
$data = array_merge(
|
||||
$this->initializeTemplateVars(),
|
||||
|
@ -106,7 +110,7 @@ public function index(Request $request, Response $response): Response
|
|||
'result_count' => count($linksToDisplay),
|
||||
'search_term' => escape($searchTerm),
|
||||
'search_tags' => escape($searchTags),
|
||||
'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)),
|
||||
'search_tags_url' => $searchTagsUrlEncoded,
|
||||
'visibility' => $visibility,
|
||||
'links' => $linkDisp,
|
||||
]
|
||||
|
@ -119,8 +123,9 @@ public function index(Request $request, Response $response): Response
|
|||
return '[' . $tag . ']';
|
||||
};
|
||||
$data['pagetitle'] .= ! empty($searchTags)
|
||||
? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
|
||||
: '';
|
||||
? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' '
|
||||
: ''
|
||||
;
|
||||
$data['pagetitle'] .= '- ';
|
||||
}
|
||||
|
||||
|
|
|
@ -47,13 +47,14 @@ public function list(Request $request, Response $response): Response
|
|||
*/
|
||||
protected function processRequest(string $type, Request $request, Response $response): Response
|
||||
{
|
||||
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||
if ($this->container->loginManager->isLoggedIn() === true) {
|
||||
$visibility = $this->container->sessionManager->getSessionParameter('visibility');
|
||||
}
|
||||
|
||||
$sort = $request->getQueryParam('sort');
|
||||
$searchTags = $request->getQueryParam('searchtags');
|
||||
$filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
|
||||
$filteringTags = $searchTags !== null ? explode($tagsSeparator, $searchTags) : [];
|
||||
|
||||
$tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
|
||||
|
||||
|
@ -71,8 +72,9 @@ protected function processRequest(string $type, Request $request, Response $resp
|
|||
$tagsUrl[escape($tag)] = urlencode((string) $tag);
|
||||
}
|
||||
|
||||
$searchTags = implode(' ', escape($filteringTags));
|
||||
$searchTagsUrl = urlencode(implode(' ', $filteringTags));
|
||||
$searchTags = tags_array2str($filteringTags, $tagsSeparator);
|
||||
$searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
|
||||
$searchTagsUrl = urlencode($searchTags);
|
||||
$data = [
|
||||
'search_tags' => escape($searchTags),
|
||||
'search_tags_url' => $searchTagsUrl,
|
||||
|
@ -82,7 +84,7 @@ protected function processRequest(string $type, Request $request, Response $resp
|
|||
$this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
|
||||
$this->assignAllView($data);
|
||||
|
||||
$searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
|
||||
$searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) .' - ' : '';
|
||||
$this->assignView(
|
||||
'pagetitle',
|
||||
$searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
||||
|
|
|
@ -45,9 +45,10 @@ public function addTag(Request $request, Response $response, array $args): Respo
|
|||
unset($params['addtag']);
|
||||
}
|
||||
|
||||
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||
// Check if this tag is already in the search query and ignore it if it is.
|
||||
// Each tag is always separated by a space
|
||||
$currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : [];
|
||||
$currentTags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
|
||||
|
||||
$addtag = true;
|
||||
foreach ($currentTags as $value) {
|
||||
|
@ -62,7 +63,7 @@ public function addTag(Request $request, Response $response, array $args): Respo
|
|||
$currentTags[] = trim($newTag);
|
||||
}
|
||||
|
||||
$params['searchtags'] = trim(implode(' ', $currentTags));
|
||||
$params['searchtags'] = tags_array2str($currentTags, $tagsSeparator);
|
||||
|
||||
// We also remove page (keeping the same page has no sense, since the results are different)
|
||||
unset($params['page']);
|
||||
|
@ -98,10 +99,11 @@ public function removeTag(Request $request, Response $response, array $args): Re
|
|||
}
|
||||
|
||||
if (isset($params['searchtags'])) {
|
||||
$tags = explode(' ', $params['searchtags']);
|
||||
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||
$tags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
|
||||
// Remove value from array $tags.
|
||||
$tags = array_diff($tags, [$tagToRemove]);
|
||||
$params['searchtags'] = implode(' ', $tags);
|
||||
$params['searchtags'] = tags_array2str($tags, $tagsSeparator);
|
||||
|
||||
if (empty($params['searchtags'])) {
|
||||
unset($params['searchtags']);
|
||||
|
|
|
@ -29,14 +29,16 @@ public function getCurlDownloadCallback(
|
|||
&$title,
|
||||
&$description,
|
||||
&$keywords,
|
||||
$retrieveDescription
|
||||
$retrieveDescription,
|
||||
$tagsSeparator
|
||||
) {
|
||||
return get_curl_download_callback(
|
||||
$charset,
|
||||
$title,
|
||||
$description,
|
||||
$keywords,
|
||||
$retrieveDescription
|
||||
$retrieveDescription,
|
||||
$tagsSeparator
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -550,7 +550,8 @@ function get_curl_download_callback(
|
|||
&$title,
|
||||
&$description,
|
||||
&$keywords,
|
||||
$retrieveDescription
|
||||
$retrieveDescription,
|
||||
$tagsSeparator
|
||||
) {
|
||||
$currentChunk = 0;
|
||||
$foundChunk = null;
|
||||
|
@ -568,6 +569,7 @@ function get_curl_download_callback(
|
|||
*/
|
||||
return function ($ch, $data) use (
|
||||
$retrieveDescription,
|
||||
$tagsSeparator,
|
||||
&$charset,
|
||||
&$title,
|
||||
&$description,
|
||||
|
@ -598,10 +600,10 @@ function get_curl_download_callback(
|
|||
if (! empty($keywords)) {
|
||||
$foundChunk = $currentChunk;
|
||||
// Keywords use the format tag1, tag2 multiple words, tag
|
||||
// So we format them to match Shaarli's separator and glue multiple words with '-'
|
||||
$keywords = implode(' ', array_map(function($keyword) {
|
||||
return implode('-', preg_split('/\s+/', trim($keyword)));
|
||||
}, explode(',', $keywords)));
|
||||
// So we split the result with `,`, then if a tag contains the separator we replace it by `-`.
|
||||
$keywords = tags_array2str(array_map(function(string $keyword) use ($tagsSeparator): string {
|
||||
return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-');
|
||||
}, tags_str2array($keywords, ',')), $tagsSeparator);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ public function retrieve(string $url): array
|
|||
$title = null;
|
||||
$description = null;
|
||||
$tags = null;
|
||||
$retrieveDescription = $this->conf->get('general.retrieve_description');
|
||||
|
||||
// Short timeout to keep the application responsive
|
||||
// The callback will fill $charset and $title with data from the downloaded page.
|
||||
|
@ -52,7 +51,8 @@ public function retrieve(string $url): array
|
|||
$title,
|
||||
$description,
|
||||
$tags,
|
||||
$retrieveDescription
|
||||
$this->conf->get('general.retrieve_description'),
|
||||
$this->conf->get('general.tags_separator', ' ')
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -585,7 +585,7 @@ public function updateMethodMigrateDatabase()
|
|||
|
||||
$linksArray = new BookmarkArray();
|
||||
foreach ($this->linkDB as $key => $link) {
|
||||
$linksArray[$key] = (new Bookmark())->fromArray($link);
|
||||
$linksArray[$key] = (new Bookmark())->fromArray($link, $this->conf->get('general.tags_separator', ' '));
|
||||
}
|
||||
$linksIo = new BookmarkIO($this->conf);
|
||||
$linksIo->write($linksArray);
|
||||
|
|
|
@ -101,11 +101,11 @@ public function import($post, UploadedFileInterface $file)
|
|||
|
||||
// Add tags to all imported bookmarks?
|
||||
if (empty($post['default_tags'])) {
|
||||
$defaultTags = array();
|
||||
$defaultTags = [];
|
||||
} else {
|
||||
$defaultTags = preg_split(
|
||||
'/[\s,]+/',
|
||||
escape($post['default_tags'])
|
||||
$defaultTags = tags_str2array(
|
||||
escape($post['default_tags']),
|
||||
$this->conf->get('general.tags_separator', ' ')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -171,7 +171,7 @@ public function import($post, UploadedFileInterface $file)
|
|||
$link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
|
||||
$link->setDescription($bkm['note']);
|
||||
$link->setPrivate($private);
|
||||
$link->setTagsString($bkm['tags']);
|
||||
$link->setTags($bkm['tags']);
|
||||
|
||||
$this->bookmarkService->addOrSet($link, false);
|
||||
$importCount++;
|
||||
|
|
|
@ -161,6 +161,7 @@ private function initialize()
|
|||
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
|
||||
|
||||
$this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
|
||||
$this->tpl->assign('tags_separator', $this->conf->get('general.tags_separator', ' '));
|
||||
|
||||
// To be removed with a proper theme configuration.
|
||||
$this->tpl->assign('conf', $this->conf);
|
||||
|
|
|
@ -42,19 +42,21 @@ function refreshToken(basePath, callback) {
|
|||
xhr.send();
|
||||
}
|
||||
|
||||
function createAwesompleteInstance(element, tags = []) {
|
||||
function createAwesompleteInstance(element, separator, tags = []) {
|
||||
const awesome = new Awesomplete(Awesomplete.$(element));
|
||||
// Tags are separated by a space
|
||||
awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
|
||||
|
||||
// Tags are separated by separator
|
||||
awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
|
||||
// Insert new selected tag in the input
|
||||
awesome.replace = (text) => {
|
||||
const before = awesome.input.value.match(/^.+ \s*|/)[0];
|
||||
awesome.input.value = `${before}${text} `;
|
||||
const before = awesome.input.value.match(new RegExp(`^.+${separator}+|`))[0];
|
||||
awesome.input.value = `${before}${text}${separator}`;
|
||||
};
|
||||
// Highlight found items
|
||||
awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(/[^ ]*$/)[0]);
|
||||
awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
|
||||
// Don't display already selected items
|
||||
const reg = /(\w+) /g;
|
||||
// WARNING: pseudo classes does not seem to work with string litterals...
|
||||
const reg = new RegExp(`([^${separator}]+)${separator}`, 'g');
|
||||
let match;
|
||||
awesome.data = (item, input) => {
|
||||
while ((match = reg.exec(input))) {
|
||||
|
@ -78,13 +80,14 @@ function createAwesompleteInstance(element, tags = []) {
|
|||
* @param selector CSS selector
|
||||
* @param tags Array of tags
|
||||
* @param instances List of existing awesomplete instances
|
||||
* @param separator Tags separator character
|
||||
*/
|
||||
function updateAwesompleteList(selector, tags, instances) {
|
||||
function updateAwesompleteList(selector, tags, instances, separator) {
|
||||
if (instances.length === 0) {
|
||||
// First load: create Awesomplete instances
|
||||
const elements = document.querySelectorAll(selector);
|
||||
[...elements].forEach((element) => {
|
||||
instances.push(createAwesompleteInstance(element, tags));
|
||||
instances.push(createAwesompleteInstance(element, separator, tags));
|
||||
});
|
||||
} else {
|
||||
// Update awesomplete tag list
|
||||
|
@ -214,6 +217,8 @@ function init(description) {
|
|||
|
||||
(() => {
|
||||
const basePath = document.querySelector('input[name="js_base_path"]').value;
|
||||
const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
|
||||
const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
|
||||
|
||||
/**
|
||||
* Handle responsive menu.
|
||||
|
@ -575,7 +580,7 @@ function init(description) {
|
|||
|
||||
// Refresh awesomplete values
|
||||
existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
|
||||
}
|
||||
};
|
||||
xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
|
||||
|
@ -615,14 +620,14 @@ function init(description) {
|
|||
refreshToken(basePath);
|
||||
|
||||
existingTags = existingTags.filter((tagItem) => tagItem !== tag);
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
|
||||
awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
|
||||
[...autocompleteFields].forEach((autocompleteField) => {
|
||||
awesomepletes.push(createAwesompleteInstance(autocompleteField));
|
||||
awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator));
|
||||
});
|
||||
|
||||
const exportForm = document.querySelector('#exportform');
|
||||
|
|
|
@ -139,6 +139,16 @@ body,
|
|||
}
|
||||
}
|
||||
|
||||
.page-form,
|
||||
.pure-alert {
|
||||
code {
|
||||
display: inline-block;
|
||||
padding: 0 2px;
|
||||
color: $dark-grey;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Make pure-extras alert closable.
|
||||
.pure-alert-closable {
|
||||
.fa-times {
|
||||
|
|
|
@ -2,29 +2,38 @@ import Awesomplete from 'awesomplete';
|
|||
import 'awesomplete/awesomplete.css';
|
||||
|
||||
(() => {
|
||||
const awp = Awesomplete.$;
|
||||
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
|
||||
[...autocompleteFields].forEach((autocompleteField) => {
|
||||
const awesomplete = new Awesomplete(awp(autocompleteField));
|
||||
awesomplete.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
|
||||
awesomplete.replace = (text) => {
|
||||
const before = awesomplete.input.value.match(/^.+ \s*|/)[0];
|
||||
awesomplete.input.value = `${before}${text} `;
|
||||
};
|
||||
awesomplete.minChars = 1;
|
||||
const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
|
||||
const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
|
||||
|
||||
autocompleteField.addEventListener('input', () => {
|
||||
const proposedTags = autocompleteField.getAttribute('data-list').replace(/,/g, '').split(' ');
|
||||
const reg = /(\w+) /g;
|
||||
let match;
|
||||
while ((match = reg.exec(autocompleteField.value)) !== null) {
|
||||
const id = proposedTags.indexOf(match[1]);
|
||||
if (id !== -1) {
|
||||
proposedTags.splice(id, 1);
|
||||
[...autocompleteFields].forEach((autocompleteField) => {
|
||||
const awesome = new Awesomplete(Awesomplete.$(autocompleteField));
|
||||
|
||||
// Tags are separated by separator
|
||||
awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(
|
||||
text,
|
||||
input.match(new RegExp(`[^${tagsSeparator}]*$`))[0],
|
||||
);
|
||||
// Insert new selected tag in the input
|
||||
awesome.replace = (text) => {
|
||||
const before = awesome.input.value.match(new RegExp(`^.+${tagsSeparator}+|`))[0];
|
||||
awesome.input.value = `${before}${text}${tagsSeparator}`;
|
||||
};
|
||||
// Highlight found items
|
||||
awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]);
|
||||
|
||||
// Don't display already selected items
|
||||
// WARNING: pseudo classes does not seem to work with string litterals...
|
||||
const reg = new RegExp(`([^${tagsSeparator}]+)${tagsSeparator}`, 'g');
|
||||
let match;
|
||||
awesome.data = (item, input) => {
|
||||
while ((match = reg.exec(input))) {
|
||||
if (item === match[1]) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
awesomplete.list = proposedTags;
|
||||
});
|
||||
return item;
|
||||
};
|
||||
awesome.minChars = 1;
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"katzgrau/klogger": "^1.2",
|
||||
"malkusch/lock": "^2.1",
|
||||
"pubsubhubbub/publisher": "dev-master",
|
||||
"shaarli/netscape-bookmark-parser": "^2.1",
|
||||
"shaarli/netscape-bookmark-parser": "^3.0",
|
||||
"slim/slim": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
|
65
composer.lock
generated
65
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "61360efbb2e1ba4c4fe00ce1f7a78ec5",
|
||||
"content-hash": "83852dec81e299a117a81206a5091472",
|
||||
"packages": [
|
||||
{
|
||||
"name": "arthurhoaro/web-thumbnailer",
|
||||
|
@ -786,24 +786,25 @@
|
|||
},
|
||||
{
|
||||
"name": "shaarli/netscape-bookmark-parser",
|
||||
"version": "v2.2.0",
|
||||
"version": "v3.0.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/shaarli/netscape-bookmark-parser.git",
|
||||
"reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df"
|
||||
"reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/432a010af2bb1832d6fbc4763e6b0100b980a1df",
|
||||
"reference": "432a010af2bb1832d6fbc4763e6b0100b980a1df",
|
||||
"url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/d2321f30413944b2d0a9844bf8cc588c71ae6305",
|
||||
"reference": "d2321f30413944b2d0a9844bf8cc588c71ae6305",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"katzgrau/klogger": "~1.0",
|
||||
"php": ">=5.6"
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.0"
|
||||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
|
@ -839,9 +840,9 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/shaarli/netscape-bookmark-parser/issues",
|
||||
"source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v2.2.0"
|
||||
"source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v3.0.1"
|
||||
},
|
||||
"time": "2020-06-06T15:53:53+00:00"
|
||||
"time": "2020-11-03T12:27:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "slim/slim",
|
||||
|
@ -1713,12 +1714,12 @@
|
|||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Roave/SecurityAdvisories.git",
|
||||
"reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff"
|
||||
"reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ba5d234b3a1559321b816b64aafc2ce6728799ff",
|
||||
"reference": "ba5d234b3a1559321b816b64aafc2ce6728799ff",
|
||||
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/065a018d3b5c2c84a53db3347cca4e1b7fa362a6",
|
||||
"reference": "065a018d3b5c2c84a53db3347cca4e1b7fa362a6",
|
||||
"shasum": ""
|
||||
},
|
||||
"conflict": {
|
||||
|
@ -1734,7 +1735,7 @@
|
|||
"bagisto/bagisto": "<0.1.5",
|
||||
"barrelstrength/sprout-base-email": "<1.2.7",
|
||||
"barrelstrength/sprout-forms": "<3.9",
|
||||
"baserproject/basercms": ">=4,<=4.3.6",
|
||||
"baserproject/basercms": ">=4,<=4.3.6|>=4.4,<4.4.1",
|
||||
"bolt/bolt": "<3.7.1",
|
||||
"brightlocal/phpwhois": "<=4.2.5",
|
||||
"buddypress/buddypress": "<5.1.2",
|
||||
|
@ -1818,6 +1819,7 @@
|
|||
"magento/magento1ee": ">=1,<1.14.4.3",
|
||||
"magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
|
||||
"marcwillmann/turn": "<0.3.3",
|
||||
"mediawiki/core": ">=1.31,<1.31.9|>=1.32,<1.32.4|>=1.33,<1.33.3|>=1.34,<1.34.3|>=1.34.99,<1.35",
|
||||
"mittwald/typo3_forum": "<1.2.1",
|
||||
"monolog/monolog": ">=1.8,<1.12",
|
||||
"namshi/jose": "<2.2",
|
||||
|
@ -1832,7 +1834,8 @@
|
|||
"onelogin/php-saml": "<2.10.4",
|
||||
"oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
|
||||
"openid/php-openid": "<2.3",
|
||||
"openmage/magento-lts": "<19.4.6|>=20,<20.0.2",
|
||||
"openmage/magento-lts": "<19.4.8|>=20,<20.0.4",
|
||||
"orchid/platform": ">=9,<9.4.4",
|
||||
"oro/crm": ">=1.7,<1.7.4",
|
||||
"oro/platform": ">=1.7,<1.7.4",
|
||||
"padraic/humbug_get_contents": "<1.1.2",
|
||||
|
@ -1867,8 +1870,8 @@
|
|||
"scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
|
||||
"sensiolabs/connect": "<4.2.3",
|
||||
"serluck/phpwhois": "<=4.2.6",
|
||||
"shopware/core": "<=6.3.1",
|
||||
"shopware/platform": "<=6.3.1",
|
||||
"shopware/core": "<=6.3.2",
|
||||
"shopware/platform": "<=6.3.2",
|
||||
"shopware/shopware": "<5.3.7",
|
||||
"silverstripe/admin": ">=1.0.3,<1.0.4|>=1.1,<1.1.1",
|
||||
"silverstripe/assets": ">=1,<1.4.7|>=1.5,<1.5.2",
|
||||
|
@ -1901,7 +1904,7 @@
|
|||
"sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
|
||||
"sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
|
||||
"sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
|
||||
"sylius/sylius": "<1.3.16|>=1.4,<1.4.12|>=1.5,<1.5.9|>=1.6,<1.6.5",
|
||||
"sylius/sylius": "<1.6.9|>=1.7,<1.7.9|>=1.8,<1.8.3",
|
||||
"symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
|
||||
"symbiote/silverstripe-versionedfiles": "<=2.0.3",
|
||||
"symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
|
||||
|
@ -2018,7 +2021,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-10-08T21:02:27+00:00"
|
||||
"time": "2020-11-01T20:01:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/code-unit-reverse-lookup",
|
||||
|
@ -2632,16 +2635,16 @@
|
|||
},
|
||||
{
|
||||
"name": "squizlabs/php_codesniffer",
|
||||
"version": "3.5.6",
|
||||
"version": "3.5.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
|
||||
"reference": "e97627871a7eab2f70e59166072a6b767d5834e0"
|
||||
"reference": "9d583721a7157ee997f235f327de038e7ea6dac4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0",
|
||||
"reference": "e97627871a7eab2f70e59166072a6b767d5834e0",
|
||||
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4",
|
||||
"reference": "9d583721a7157ee997f235f327de038e7ea6dac4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2684,24 +2687,24 @@
|
|||
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
|
||||
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
|
||||
},
|
||||
"time": "2020-08-10T04:50:15+00:00"
|
||||
"time": "2020-10-23T02:01:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.18.1",
|
||||
"version": "v1.20.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||
"reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
|
||||
"reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
|
||||
"reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41",
|
||||
"reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "For best performance"
|
||||
|
@ -2709,7 +2712,7 @@
|
|||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.18-dev"
|
||||
"dev-main": "1.20-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
|
@ -2747,7 +2750,7 @@
|
|||
"portable"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.18.0"
|
||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2763,7 +2766,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-07-14T12:35:20+00:00"
|
||||
"time": "2020-10-23T14:02:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
|
|
|
@ -74,6 +74,7 @@ Some settings can be configured directly from a web browser by accesing the `Too
|
|||
"timezone": "Europe\/Paris",
|
||||
"title": "My Shaarli",
|
||||
"header_link": "?"
|
||||
"tags_separator": " "
|
||||
},
|
||||
"dev": {
|
||||
"debug": false,
|
||||
|
@ -153,6 +154,7 @@ _These settings should not be edited_
|
|||
- **enable_async_metadata** (boolean): Retrieve external bookmark metadata asynchronously to prevent bookmark creation slowdown.
|
||||
- **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags.
|
||||
- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
|
||||
- **tags_separator**: Defines your tags separator (default: whitespace).
|
||||
|
||||
### Security
|
||||
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
$this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
|
||||
$this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
|
||||
$this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
|
||||
$this->post('/tags/change-separator', '\Shaarli\Front\Controller\Admin\ManageTagController:changeSeparator');
|
||||
$this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare');
|
||||
$this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm');
|
||||
$this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm');
|
||||
|
|
|
@ -44,7 +44,7 @@ public static function setUpBeforeClass(): void
|
|||
self::$refDB->write(self::$testDatastore);
|
||||
$history = new History('sandbox/history.php');
|
||||
self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true);
|
||||
self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks());
|
||||
self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -78,6 +78,23 @@ public function testFromArrayMinimal()
|
|||
$this->assertTrue($bookmark->isNote());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test fromArray() with a link with a custom tags separator
|
||||
*/
|
||||
public function testFromArrayCustomTagsSeparator()
|
||||
{
|
||||
$data = [
|
||||
'id' => 1,
|
||||
'tags' => ['tag1', 'tag2', 'chair'],
|
||||
];
|
||||
|
||||
$bookmark = (new Bookmark())->fromArray($data, '@');
|
||||
$this->assertEquals($data['id'], $bookmark->getId());
|
||||
$this->assertEquals($data['tags'], $bookmark->getTags());
|
||||
$this->assertEquals('tag1@tag2@chair', $bookmark->getTagsString('@'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test validate() with a valid minimal bookmark
|
||||
*/
|
||||
|
@ -252,7 +269,7 @@ public function testSetTagsString()
|
|||
{
|
||||
$bookmark = new Bookmark();
|
||||
|
||||
$str = 'tag1 tag2 tag3.tag3-2, tag4 , -tag5 ';
|
||||
$str = 'tag1 tag2 tag3.tag3-2 tag4 -tag5 ';
|
||||
$bookmark->setTagsString($str);
|
||||
$this->assertEquals(
|
||||
[
|
||||
|
@ -276,9 +293,9 @@ public function testSetTags()
|
|||
$array = [
|
||||
'tag1 ',
|
||||
' tag2',
|
||||
'tag3.tag3-2,',
|
||||
', tag4',
|
||||
', ',
|
||||
'tag3.tag3-2',
|
||||
' tag4',
|
||||
' ',
|
||||
'-tag5 ',
|
||||
];
|
||||
$bookmark->setTags($array);
|
||||
|
|
|
@ -277,7 +277,8 @@ public function testCurlDownloadCallbackOk(): void
|
|||
$title,
|
||||
$desc,
|
||||
$keywords,
|
||||
false
|
||||
false,
|
||||
' '
|
||||
);
|
||||
|
||||
$data = [
|
||||
|
@ -327,7 +328,8 @@ public function testCurlDownloadCallbackOkNoCharset(): void
|
|||
$title,
|
||||
$desc,
|
||||
$keywords,
|
||||
false
|
||||
false,
|
||||
' '
|
||||
);
|
||||
|
||||
$data = [
|
||||
|
@ -360,7 +362,8 @@ public function testCurlDownloadCallbackOkHtmlCharset(): void
|
|||
$title,
|
||||
$desc,
|
||||
$keywords,
|
||||
false
|
||||
false,
|
||||
' '
|
||||
);
|
||||
|
||||
$data = [
|
||||
|
@ -393,7 +396,8 @@ public function testCurlDownloadCallbackOkNoTitle(): void
|
|||
$title,
|
||||
$desc,
|
||||
$keywords,
|
||||
false
|
||||
false,
|
||||
' '
|
||||
);
|
||||
|
||||
$data = [
|
||||
|
@ -458,7 +462,8 @@ public function testCurlDownloadCallbackOkWithDesc(): void
|
|||
$title,
|
||||
$desc,
|
||||
$keywords,
|
||||
true
|
||||
true,
|
||||
' '
|
||||
);
|
||||
$data = [
|
||||
'th=device-width">'
|
||||
|
@ -604,6 +609,115 @@ public function testIsNotNote()
|
|||
$this->assertFalse(is_note('https://github.com/shaarli/Shaarli/?hi'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags_str2array with whitespace separator.
|
||||
*/
|
||||
public function testTagsStr2ArrayWithSpaceSeparator(): void
|
||||
{
|
||||
$separator = ' ';
|
||||
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator));
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1 tag2 tag3', $separator));
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array(' tag1 tag2 tag3 ', $separator));
|
||||
static::assertSame(['tag1@', 'tag2,', '.tag3'], tags_str2array(' tag1@ tag2, .tag3 ', $separator));
|
||||
static::assertSame([], tags_str2array('', $separator));
|
||||
static::assertSame([], tags_str2array(' ', $separator));
|
||||
static::assertSame([], tags_str2array(null, $separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags_str2array with @ separator.
|
||||
*/
|
||||
public function testTagsStr2ArrayWithCharSeparator(): void
|
||||
{
|
||||
$separator = '@';
|
||||
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@tag2@tag3', $separator));
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('tag1@@@@tag2@@@@tag3', $separator));
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_str2array('@@@tag1@@@tag2@@@@tag3@@', $separator));
|
||||
static::assertSame(
|
||||
['tag1#', 'tag2, and other', '.tag3'],
|
||||
tags_str2array('@@@ tag1# @@@ tag2, and other @@@@.tag3@@', $separator)
|
||||
);
|
||||
static::assertSame([], tags_str2array('', $separator));
|
||||
static::assertSame([], tags_str2array(' ', $separator));
|
||||
static::assertSame([], tags_str2array(null, $separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags_array2str with ' ' separator.
|
||||
*/
|
||||
public function testTagsArray2StrWithSpaceSeparator(): void
|
||||
{
|
||||
$separator = ' ';
|
||||
|
||||
static::assertSame('tag1 tag2 tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator));
|
||||
static::assertSame('tag1, tag2@ tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator));
|
||||
static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', 'tag2', 'tag3 '], $separator));
|
||||
static::assertSame('tag1 tag2 tag3', tags_array2str([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator));
|
||||
static::assertSame('tag1', tags_array2str([' tag1 '], $separator));
|
||||
static::assertSame('', tags_array2str([' '], $separator));
|
||||
static::assertSame('', tags_array2str([], $separator));
|
||||
static::assertSame('', tags_array2str(null, $separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags_array2str with @ separator.
|
||||
*/
|
||||
public function testTagsArray2StrWithCharSeparator(): void
|
||||
{
|
||||
$separator = '@';
|
||||
|
||||
static::assertSame('tag1@tag2@tag3', tags_array2str(['tag1', 'tag2', 'tag3'], $separator));
|
||||
static::assertSame('tag1,@tag2@tag3', tags_array2str(['tag1,', 'tag2@', 'tag3'], $separator));
|
||||
static::assertSame(
|
||||
'tag1@tag2, and other@tag3',
|
||||
tags_array2str(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator)
|
||||
);
|
||||
static::assertSame('tag1@tag2@tag3', tags_array2str(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator));
|
||||
static::assertSame('tag1', tags_array2str(['@@@@tag1@@@@'], $separator));
|
||||
static::assertSame('', tags_array2str(['@@@'], $separator));
|
||||
static::assertSame('', tags_array2str([], $separator));
|
||||
static::assertSame('', tags_array2str(null, $separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags_array2str with @ separator.
|
||||
*/
|
||||
public function testTagsFilterWithSpaceSeparator(): void
|
||||
{
|
||||
$separator = ' ';
|
||||
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator));
|
||||
static::assertSame(['tag1,', 'tag2@', 'tag3'], tags_filter(['tag1,', 'tag2@', 'tag3'], $separator));
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', 'tag2', 'tag3 '], $separator));
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter([' tag1 ', ' ', 'tag2', ' ', 'tag3 '], $separator));
|
||||
static::assertSame(['tag1'], tags_filter([' tag1 '], $separator));
|
||||
static::assertSame([], tags_filter([' '], $separator));
|
||||
static::assertSame([], tags_filter([], $separator));
|
||||
static::assertSame([], tags_filter(null, $separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test tags_array2str with @ separator.
|
||||
*/
|
||||
public function testTagsArrayFilterWithSpaceSeparator(): void
|
||||
{
|
||||
$separator = '@';
|
||||
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['tag1', 'tag2', 'tag3'], $separator));
|
||||
static::assertSame(['tag1,', 'tag2#', 'tag3'], tags_filter(['tag1,', 'tag2#', 'tag3'], $separator));
|
||||
static::assertSame(
|
||||
['tag1', 'tag2, and other', 'tag3'],
|
||||
tags_filter(['@@@@ tag1@@@', ' @tag2, and other @', 'tag3@@@@'], $separator)
|
||||
);
|
||||
static::assertSame(['tag1', 'tag2', 'tag3'], tags_filter(['@@@tag1@@@', '@', 'tag2', '@@@', 'tag3@@@'], $separator));
|
||||
static::assertSame(['tag1'], tags_filter(['@@@@tag1@@@@'], $separator));
|
||||
static::assertSame([], tags_filter(['@@@'], $separator));
|
||||
static::assertSame([], tags_filter([], $separator));
|
||||
static::assertSame([], tags_filter(null, $separator));
|
||||
}
|
||||
|
||||
/**
|
||||
* Util function to build an hashtag link.
|
||||
*
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
use Shaarli\Bookmark\Bookmark;
|
||||
use Shaarli\Bookmark\BookmarkFilter;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Front\Exception\WrongTokenException;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Shaarli\TestCase;
|
||||
|
@ -44,9 +45,32 @@ public function testIndex(): void
|
|||
static::assertSame('changetag', (string) $result->getBody());
|
||||
|
||||
static::assertSame('fromtag', $assignedVariables['fromtag']);
|
||||
static::assertSame('@', $assignedVariables['tags_separator']);
|
||||
static::assertSame('Manage tags - Shaarli', $assignedVariables['pagetitle']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test displaying manage tag page
|
||||
*/
|
||||
public function testIndexWhitespaceSeparator(): void
|
||||
{
|
||||
$assignedVariables = [];
|
||||
$this->assignTemplateVars($assignedVariables);
|
||||
|
||||
$this->container->conf = $this->createMock(ConfigManager::class);
|
||||
$this->container->conf->method('get')->willReturnCallback(function (string $key) {
|
||||
return $key === 'general.tags_separator' ? ' ' : $key;
|
||||
});
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
$this->controller->index($request, $response);
|
||||
|
||||
static::assertSame(' ', $assignedVariables['tags_separator']);
|
||||
static::assertSame('whitespace', $assignedVariables['tags_separator_desc']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test posting a tag update - rename tag - valid info provided.
|
||||
*/
|
||||
|
@ -269,4 +293,116 @@ public function testSaveRenameTagMissingTo(): void
|
|||
static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
|
||||
static::assertSame(['Invalid tags provided.'], $session[SessionManager::KEY_WARNING_MESSAGES]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test changeSeparator to '#': redirection + success message.
|
||||
*/
|
||||
public function testChangeSeparatorValid(): void
|
||||
{
|
||||
$toSeparator = '#';
|
||||
|
||||
$session = [];
|
||||
$this->assignSessionVars($session);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->expects(static::atLeastOnce())
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($toSeparator): ?string {
|
||||
return $key === 'separator' ? $toSeparator : $key;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->conf
|
||||
->expects(static::once())
|
||||
->method('set')
|
||||
->with('general.tags_separator', $toSeparator, true, true)
|
||||
;
|
||||
|
||||
$result = $this->controller->changeSeparator($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
|
||||
|
||||
static::assertArrayNotHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
|
||||
static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
|
||||
static::assertArrayHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
|
||||
static::assertSame(
|
||||
['Your tags separator setting has been updated!'],
|
||||
$session[SessionManager::KEY_SUCCESS_MESSAGES]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test changeSeparator to '#@' (too long): redirection + error message.
|
||||
*/
|
||||
public function testChangeSeparatorInvalidTooLong(): void
|
||||
{
|
||||
$toSeparator = '#@';
|
||||
|
||||
$session = [];
|
||||
$this->assignSessionVars($session);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->expects(static::atLeastOnce())
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($toSeparator): ?string {
|
||||
return $key === 'separator' ? $toSeparator : $key;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->conf->expects(static::never())->method('set');
|
||||
|
||||
$result = $this->controller->changeSeparator($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
|
||||
|
||||
static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
|
||||
static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
|
||||
static::assertArrayHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
|
||||
static::assertSame(
|
||||
['Tags separator must be a single character.'],
|
||||
$session[SessionManager::KEY_ERROR_MESSAGES]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test changeSeparator to '#@' (too long): redirection + error message.
|
||||
*/
|
||||
public function testChangeSeparatorInvalidReservedCharacter(): void
|
||||
{
|
||||
$toSeparator = '*';
|
||||
|
||||
$session = [];
|
||||
$this->assignSessionVars($session);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->expects(static::atLeastOnce())
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($toSeparator): ?string {
|
||||
return $key === 'separator' ? $toSeparator : $key;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->conf->expects(static::never())->method('set');
|
||||
|
||||
$result = $this->controller->changeSeparator($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/admin/tags'], $result->getHeader('location'));
|
||||
|
||||
static::assertArrayNotHasKey(SessionManager::KEY_SUCCESS_MESSAGES, $session);
|
||||
static::assertArrayNotHasKey(SessionManager::KEY_WARNING_MESSAGES, $session);
|
||||
static::assertArrayHasKey(SessionManager::KEY_ERROR_MESSAGES, $session);
|
||||
static::assertStringStartsWith(
|
||||
'These characters are reserved and can\'t be used as tags separator',
|
||||
$session[SessionManager::KEY_ERROR_MESSAGES][0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ public function testDisplayCreateFormWithUrlAndWithMetadataRetrieval(): void
|
|||
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
|
||||
static::assertSame($remoteTitle, $assignedVariables['link']['title']);
|
||||
static::assertSame($remoteDesc, $assignedVariables['link']['description']);
|
||||
static::assertSame($remoteTags, $assignedVariables['link']['tags']);
|
||||
static::assertSame($remoteTags . ' ', $assignedVariables['link']['tags']);
|
||||
static::assertFalse($assignedVariables['link']['private']);
|
||||
|
||||
static::assertTrue($assignedVariables['link_is_new']);
|
||||
|
@ -192,7 +192,7 @@ public function testDisplayCreateFormWithFullParameters(): void
|
|||
'post' => 'http://url.tld/other?part=3&utm_ad=pay#hash',
|
||||
'title' => 'Provided Title',
|
||||
'description' => 'Provided description.',
|
||||
'tags' => 'abc def',
|
||||
'tags' => 'abc@def',
|
||||
'private' => '1',
|
||||
'source' => 'apps',
|
||||
];
|
||||
|
@ -216,7 +216,7 @@ public function testDisplayCreateFormWithFullParameters(): void
|
|||
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
|
||||
static::assertSame($parameters['title'], $assignedVariables['link']['title']);
|
||||
static::assertSame($parameters['description'], $assignedVariables['link']['description']);
|
||||
static::assertSame($parameters['tags'], $assignedVariables['link']['tags']);
|
||||
static::assertSame($parameters['tags'] . '@', $assignedVariables['link']['tags']);
|
||||
static::assertTrue($assignedVariables['link']['private']);
|
||||
static::assertTrue($assignedVariables['link_is_new']);
|
||||
static::assertSame($parameters['source'], $assignedVariables['source']);
|
||||
|
@ -360,7 +360,7 @@ public function testDisplayCreateFormWithExistingUrl(): void
|
|||
static::assertSame($expectedUrl, $assignedVariables['link']['url']);
|
||||
static::assertSame($title, $assignedVariables['link']['title']);
|
||||
static::assertSame($description, $assignedVariables['link']['description']);
|
||||
static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
|
||||
static::assertSame(implode('@', $tags) . '@', $assignedVariables['link']['tags']);
|
||||
static::assertTrue($assignedVariables['link']['private']);
|
||||
static::assertSame($createdAt, $assignedVariables['link']['created']);
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ public function testDisplayEditFormDefault(): void
|
|||
static::assertSame($url, $assignedVariables['link']['url']);
|
||||
static::assertSame($title, $assignedVariables['link']['title']);
|
||||
static::assertSame($description, $assignedVariables['link']['description']);
|
||||
static::assertSame(implode(' ', $tags), $assignedVariables['link']['tags']);
|
||||
static::assertSame(implode('@', $tags) . '@', $assignedVariables['link']['tags']);
|
||||
static::assertTrue($assignedVariables['link']['private']);
|
||||
static::assertSame($createdAt, $assignedVariables['link']['created']);
|
||||
}
|
||||
|
|
|
@ -173,7 +173,7 @@ public function testIndexDefaultWithFilters(): void
|
|||
$request = $this->createMock(Request::class);
|
||||
$request->method('getParam')->willReturnCallback(function (string $key) {
|
||||
if ('searchtags' === $key) {
|
||||
return 'abc def';
|
||||
return 'abc@def';
|
||||
}
|
||||
if ('searchterm' === $key) {
|
||||
return 'ghi jkl';
|
||||
|
@ -204,7 +204,7 @@ public function testIndexDefaultWithFilters(): void
|
|||
->expects(static::once())
|
||||
->method('search')
|
||||
->with(
|
||||
['searchtags' => 'abc def', 'searchterm' => 'ghi jkl'],
|
||||
['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'],
|
||||
'private',
|
||||
false,
|
||||
true
|
||||
|
@ -222,7 +222,7 @@ public function testIndexDefaultWithFilters(): void
|
|||
static::assertSame('linklist', (string) $result->getBody());
|
||||
|
||||
static::assertSame('Search: ghi jkl [abc] [def] - Shaarli', $assignedVariables['pagetitle']);
|
||||
static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc+def', $assignedVariables['previous_page_url']);
|
||||
static::assertSame('?page=2&searchterm=ghi+jkl&searchtags=abc%40def', $assignedVariables['previous_page_url']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -41,6 +41,10 @@ protected function createContainer(): void
|
|||
// Config
|
||||
$this->container->conf = $this->createMock(ConfigManager::class);
|
||||
$this->container->conf->method('get')->willReturnCallback(function (string $parameter, $default) {
|
||||
if ($parameter === 'general.tags_separator') {
|
||||
return '@';
|
||||
}
|
||||
|
||||
return $default === null ? $parameter : $default;
|
||||
});
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ public function testValidCloudControllerInvokeWithParameters(): void
|
|||
->with()
|
||||
->willReturnCallback(function (string $key): ?string {
|
||||
if ('searchtags' === $key) {
|
||||
return 'ghi def';
|
||||
return 'ghi@def';
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -131,7 +131,7 @@ public function testValidCloudControllerInvokeWithParameters(): void
|
|||
->withConsecutive(['render_tagcloud'])
|
||||
->willReturnCallback(function (string $hook, array $data, array $param): array {
|
||||
if ('render_tagcloud' === $hook) {
|
||||
static::assertSame('ghi def', $data['search_tags']);
|
||||
static::assertSame('ghi@def@', $data['search_tags']);
|
||||
static::assertCount(1, $data['tags']);
|
||||
|
||||
static::assertArrayHasKey('loggedin', $param);
|
||||
|
@ -147,7 +147,7 @@ public function testValidCloudControllerInvokeWithParameters(): void
|
|||
static::assertSame('tag.cloud', (string) $result->getBody());
|
||||
static::assertSame('ghi def - Tag cloud - Shaarli', $assignedVariables['pagetitle']);
|
||||
|
||||
static::assertSame('ghi def', $assignedVariables['search_tags']);
|
||||
static::assertSame('ghi@def@', $assignedVariables['search_tags']);
|
||||
static::assertCount(1, $assignedVariables['tags']);
|
||||
|
||||
static::assertArrayHasKey('abc', $assignedVariables['tags']);
|
||||
|
@ -277,7 +277,7 @@ public function testValidListControllerInvokeWithParameters(): void
|
|||
->with()
|
||||
->willReturnCallback(function (string $key): ?string {
|
||||
if ('searchtags' === $key) {
|
||||
return 'ghi def';
|
||||
return 'ghi@def';
|
||||
} elseif ('sort' === $key) {
|
||||
return 'alpha';
|
||||
}
|
||||
|
@ -310,7 +310,7 @@ public function testValidListControllerInvokeWithParameters(): void
|
|||
->withConsecutive(['render_taglist'])
|
||||
->willReturnCallback(function (string $hook, array $data, array $param): array {
|
||||
if ('render_taglist' === $hook) {
|
||||
static::assertSame('ghi def', $data['search_tags']);
|
||||
static::assertSame('ghi@def@', $data['search_tags']);
|
||||
static::assertCount(1, $data['tags']);
|
||||
|
||||
static::assertArrayHasKey('loggedin', $param);
|
||||
|
@ -326,7 +326,7 @@ public function testValidListControllerInvokeWithParameters(): void
|
|||
static::assertSame('tag.list', (string) $result->getBody());
|
||||
static::assertSame('ghi def - Tag list - Shaarli', $assignedVariables['pagetitle']);
|
||||
|
||||
static::assertSame('ghi def', $assignedVariables['search_tags']);
|
||||
static::assertSame('ghi@def@', $assignedVariables['search_tags']);
|
||||
static::assertCount(1, $assignedVariables['tags']);
|
||||
static::assertSame(3, $assignedVariables['tags']['abc']);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ public function testAddTagWithRefererAndExistingSearch(): void
|
|||
|
||||
static::assertInstanceOf(Response::class, $result);
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
|
||||
static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
public function testAddTagWithoutRefererAndExistingSearch(): void
|
||||
|
@ -80,7 +80,7 @@ public function testAddTagRemoveLegacyQueryParam(): void
|
|||
|
||||
static::assertInstanceOf(Response::class, $result);
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
|
||||
static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
public function testAddTagResetPagination(): void
|
||||
|
@ -96,7 +96,7 @@ public function testAddTagResetPagination(): void
|
|||
|
||||
static::assertInstanceOf(Response::class, $result);
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/controller/?searchtags=def+abc'], $result->getHeader('location'));
|
||||
static::assertSame(['/controller/?searchtags=def%40abc'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
public function testAddTagWithRefererAndEmptySearch(): void
|
||||
|
|
|
@ -531,7 +531,7 @@ public function testSetDefaultTags()
|
|||
{
|
||||
$post = array(
|
||||
'privacy' => 'public',
|
||||
'default_tags' => 'tag1,tag2 tag3'
|
||||
'default_tags' => 'tag1 tag2 tag3'
|
||||
);
|
||||
$files = file2array('netscape_basic.htm');
|
||||
$this->assertStringMatchesFormat(
|
||||
|
@ -552,7 +552,7 @@ public function testSanitizeDefaultTags()
|
|||
{
|
||||
$post = array(
|
||||
'privacy' => 'public',
|
||||
'default_tags' => 'tag1&,tag2 "tag3"'
|
||||
'default_tags' => 'tag1& tag2 "tag3"'
|
||||
);
|
||||
$files = file2array('netscape_basic.htm');
|
||||
$this->assertStringMatchesFormat(
|
||||
|
@ -572,6 +572,43 @@ public function testSanitizeDefaultTags()
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user-specified tags to all imported bookmarks
|
||||
*/
|
||||
public function testSetDefaultTagsWithCustomSeparator()
|
||||
{
|
||||
$separator = '@';
|
||||
$this->conf->set('general.tags_separator', $separator);
|
||||
$post = [
|
||||
'privacy' => 'public',
|
||||
'default_tags' => 'tag1@tag2@tag3@multiple words tag'
|
||||
];
|
||||
$files = file2array('netscape_basic.htm');
|
||||
$this->assertStringMatchesFormat(
|
||||
'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
|
||||
.' 2 bookmarks imported, 0 bookmarks overwritten, 0 bookmarks skipped.',
|
||||
$this->netscapeBookmarkUtils->import($post, $files)
|
||||
);
|
||||
$this->assertEquals(2, $this->bookmarkService->count());
|
||||
$this->assertEquals(0, $this->bookmarkService->count(BookmarkFilter::$PRIVATE));
|
||||
$this->assertEquals(
|
||||
'tag1@tag2@tag3@multiple words tag@private@secret',
|
||||
$this->bookmarkService->get(0)->getTagsString($separator)
|
||||
);
|
||||
$this->assertEquals(
|
||||
['tag1', 'tag2', 'tag3', 'multiple words tag', 'private', 'secret'],
|
||||
$this->bookmarkService->get(0)->getTags()
|
||||
);
|
||||
$this->assertEquals(
|
||||
'tag1@tag2@tag3@multiple words tag@public@hello@world',
|
||||
$this->bookmarkService->get(1)->getTagsString($separator)
|
||||
);
|
||||
$this->assertEquals(
|
||||
['tag1', 'tag2', 'tag3', 'multiple words tag', 'public', 'hello', 'world'],
|
||||
$this->bookmarkService->get(1)->getTags()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure each imported bookmark has a unique id
|
||||
*
|
||||
|
|
|
@ -36,6 +36,29 @@ <h2 class="window-title">{"Manage tags"|t}</h2>
|
|||
<p>{'You can also edit tags in the'|t} <a href="{$base_path}/tags/list?sort=usage">{'tag list'|t}</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
|
||||
<div class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24">
|
||||
<h2 class="window-title">{"Change tags separator"|t}</h2>
|
||||
<form method="POST" action="{$base_path}/admin/tags/change-separator" name="changeseparator" id="changeseparator">
|
||||
<p>
|
||||
{'Your current tag separator is'|t} <code>{$tags_separator}</code>{if="!empty($tags_separator_desc)"} ({$tags_separator_desc}){/if}.
|
||||
</p>
|
||||
<div>
|
||||
<input type="text" name="separator" placeholder="{'New separator'|t}"
|
||||
id="separator">
|
||||
</div>
|
||||
<input type="hidden" name="token" value="{$token}">
|
||||
<div>
|
||||
<input type="submit" value="{'Save'|t}" name="saveseparator">
|
||||
</div>
|
||||
<p>
|
||||
{'Note that hashtags won\'t fully work with a non-whitespace separator.'|t}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{include="page.footer"}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
{'for'|t} <em><strong>{$search_term}</strong></em>
|
||||
{/if}
|
||||
{if="!empty($search_tags)"}
|
||||
{$exploded_tags=explode(' ', $search_tags)}
|
||||
{$exploded_tags=tags_str2array($search_tags, $tags_separator)}
|
||||
{'tagged'|t}
|
||||
{loop="$exploded_tags"}
|
||||
<span class="label label-tag" title="{'Remove tag'|t}">
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
<div class="pure-u-2-24"></div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="token" value="{$token}" id="token" />
|
||||
|
||||
{loop="$plugins_footer.endofpage"}
|
||||
{$value}
|
||||
{/loop}
|
||||
|
@ -41,4 +39,7 @@
|
|||
</div>
|
||||
|
||||
<input type="hidden" name="js_base_path" value="{$base_path}" />
|
||||
<input type="hidden" name="token" value="{$token}" id="token" />
|
||||
<input type="hidden" name="tags_separator" value="{$tags_separator}" id="tags_separator" />
|
||||
|
||||
<script src="{$asset_path}/js/shaarli.min.js?v={$version_hash}#"></script>
|
||||
|
|
|
@ -48,7 +48,7 @@ <h2 class="window-title">{'Tag cloud'|t} - {$countTags} {'tags'|t}</h2>
|
|||
|
||||
<div id="cloudtag" class="cloudtag-container">
|
||||
{loop="tags"}
|
||||
<a href="{$base_path}/?searchtags={$tags_url.$key1} {$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
|
||||
<a href="{$base_path}/?searchtags={$tags_url.$key1}{$tags_separator|urlencode}{$search_tags_url}" style="font-size:{$value.size}em;">{$key}</a
|
||||
><a href="{$base_path}/add-tag/{$tags_url.$key1}" title="{'Filter by tag'|t}" class="count">{$value.count}</a>
|
||||
{loop="$value.tag_plugin"}
|
||||
{$value}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<meta name="referrer" content="same-origin">
|
||||
<link rel="alternate" type="application/rss+xml" href="{$feedurl}feed/rss?{$searchcrits}#" title="RSS Feed" />
|
||||
<link rel="alternate" type="application/atom+xml" href="{$feedurl}feed/atom?{$searchcrits}#" title="ATOM Feed" />
|
||||
<link href="img/favicon.ico" rel="shortcut icon" type="image/x-icon" />
|
||||
<link href="{$asset_path}/img/favicon.ico#" rel="shortcut icon" type="image/x-icon" />
|
||||
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/shaarli.min.css#" />
|
||||
{if="$formatter==='markdown'"}
|
||||
<link type="text/css" rel="stylesheet" href="{$asset_path}/css/markdown.min.css?v={$version_hash}#" />
|
||||
{/if}
|
||||
{loop="$plugins_includes.css_files"}
|
||||
<link type="text/css" rel="stylesheet" href="{$base_path}/{$value}#"/>
|
||||
<link type="text/css" rel="stylesheet" href="{$root_path}/{$value}#"/>
|
||||
{/loop}
|
||||
{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />{/if}
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
for <em>{$search_term}</em>
|
||||
{/if}
|
||||
{if="!empty($search_tags)"}
|
||||
{$exploded_tags=explode(' ', $search_tags)}
|
||||
{$exploded_tags=tags_str2array($search_tags, $tags_separator)}
|
||||
tagged
|
||||
{loop="$exploded_tags"}
|
||||
<span class="linktag" title="Remove tag">
|
||||
|
|
|
@ -23,8 +23,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<script src="{$asset_path}/js/shaarli.min.js#"></script>
|
||||
|
||||
{if="$is_logged_in"}
|
||||
<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
|
||||
{/if}
|
||||
|
@ -34,3 +32,7 @@
|
|||
{/loop}
|
||||
|
||||
<input type="hidden" name="js_base_path" value="{$base_path}" />
|
||||
<input type="hidden" name="token" value="{$token}" id="token" />
|
||||
<input type="hidden" name="tags_separator" value="{$tags_separator}" id="tags_separator" />
|
||||
|
||||
<script src="{$asset_path}/js/shaarli.min.js#"></script>
|
||||
|
|
Loading…
Reference in a new issue