vendor/
# Release archives
-*.tar
+*.tar.gz
*.zip
# Development and test resources
directories:
- $HOME/.composer/cache
php:
+ - 7.1
- 7.0
- 5.6
- 5.5
and this project adheres to [Semantic Versioning](http://semver.org/).
-## [v0.8.1](https://github.com/shaarli/Shaarli/releases/tag/v0.8.1) - UNPUBLISHED
+## [v0.9.0](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0) - UNPUBLISHED
+
+### Added
+
+### Changed
+
+### Fixed
+
+
+## [v0.8.1](https://github.com/shaarli/Shaarli/releases/tag/v0.8.1) - 2016-12-12
+
+> Note: this version will create an automatic backup of your database if anything goes wrong.
+
### Added
- Add CHANGELOG.md to track the whole project's history
- Enable Composer cache for Travis builds
- Plugins:
- Add an [Isso](https://posativ.org/isso/) plugin to enable user comments on permalinks
- Allow defining init functions, e.g. for performing checks and error processing
-
-### Changed
-- Cleanup `{loop}` declarations in templates
+ - Add a Piwik plugin for analytics.
+ - Markdown: add warning notice regarding HTML rendering
+- Meta tag to *not* send the referrer to external resources.
+
+### Changed
+- Link ID complete refactoring:
+ - Links now have a numeric ID instead of dates
+ - Short URLs are now created once and can't change over time (previous URL are kept)
+- Templates:
+ - Changed placeholder behaviour for: `buttons_toolbar`, `fields_toolbar` and `action_plugin`
+ - Cleanup `{loop}` declarations in templates
+ - Tools: hide Firefox Social button when not in HTTPS
+ - Firefox Social: show Shaarli's title when shaaring using Firefox Social
- Release archives now have the same structure as GitHub-generated archives:
- archives contain a `Shaarli` directory, itself containing sources + dependencies
- the tarball is now gzipped
+- Plugins:
+ - Markdown: Parsedown library is now imported through Composer
- Minor code cleanup: PHPDoc, spelling, unused variables, etc.
+- Docker: explicitly set the maximum file upload size to 10 MiB
### Fixed
- Fix the server `<self>` value in Atom/RSS feeds
- Plugins:
- Tools: only display parameter description when it exists
- archive.org: do not propose archival of private notes
+ - Markdown:
+ - render links properly in code blocks
+ - bug regarding the `nomarkdown` tag
+ - W3C compliance
- Use absolute URL for hashtags in RSS and ATOM feeds
+- Docker: specify the location of the favicon
+- ATOM feed: remove new line between content tag and data
### Security
- Allow whitelisting trusted IPs, else continue banning clients upon login failure
License: MIT License (http://opensource.org/licenses/MIT)
Copyright: (C) 2015 Nicolas Lœuillet - https://github.com/wallabag/wallabag
+Files: plugins/markdown/Parsedown.php
+License: MIT License (http://opensource.org/licenses/MIT)
+Copyright: (C) 2015 Emanuil Rusev - https://github.com/erusev/parsedown
+
Files: tpl/default/img/sad_star.png
License: MIT License (http://opensource.org/licenses/MIT)
Copyright: (C) 2015 kalvn - https://github.com/kalvn/Shaarli-Material
-Allow from none
-Deny from all
+<IfModule version_module>
+ <IfVersion >= 2.4>
+ Require all denied
+ </IfVersion>
+ <IfVersion < 2.4>
+ Allow from none
+ Deny from all
+ </IfVersion>
+</IfModule>
+
+<IfModule !version_module>
+ Require all denied
+</IfModule>
$link['description'] = format_description($link['description'], '', $pageaddr);
$link['description'] .= PHP_EOL .'<br>— '. $permalink;
- $pubDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+ $pubDate = $link['created'];
$link['pub_iso_date'] = $this->getIsoDate($pubDate);
// atom:entry elements MUST contain exactly one atom:updated element.
if (!empty($link['updated'])) {
- $upDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['updated']);
+ $upDate = $link['updated'];
$link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
} else {
$link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);;
*
* Example:
* $myLinks = new LinkDB();
- * echo $myLinks['20110826_161819']['title'];
+ * echo $myLinks[350]['title'];
* foreach ($myLinks as $link)
* echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
*
* Available keys:
+ * - id: primary key, incremental integer identifier (persistent)
* - description: description of the entry
- * - linkdate: creation date of this entry, format: YYYYMMDD_HHMMSS
- * (e.g.'20110914_192317')
- * - updated: last modification date of this entry, format: YYYYMMDD_HHMMSS
+ * - created: creation date of this entry, DateTime object.
+ * - updated: last modification date of this entry, DateTime object.
* - private: Is this link private? 0=no, other value=yes
* - tags: tags attached to this entry (separated by spaces)
* - title Title of the link
* Can be absolute or relative.
* Relative URLs are permalinks (e.g.'?m-ukcw')
* - real_url Absolute processed URL.
+ * - shorturl Permalink smallhash
*
* Implements 3 interfaces:
* - ArrayAccess: behaves like an associative array;
* - Countable: there is a count() method;
* - Iterator: usable in foreach () loops.
+ *
+ * ID mechanism:
+ * ArrayAccess is implemented in a way that will allow to access a link
+ * with the unique identifier ID directly with $link[ID].
+ * Note that it's not the real key of the link array attribute.
+ * This mechanism is in place to have persistent link IDs,
+ * even though the internal array is reordered by date.
+ * Example:
+ * - DB: link #1 (2010-01-01) link #2 (2016-01-01)
+ * - Order: #2 #1
+ * - Import links containing: 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
*/
class LinkDB implements Iterator, Countable, ArrayAccess
{
// - value: associative array (keys: title, description...)
private $links;
- // List of all recorded URLs (key=url, value=linkdate)
- // for fast reserve search (url-->linkdate)
+ // List of all recorded URLs (key=url, value=link offset)
+ // for fast reserve search (url-->link offset)
private $urls;
- // List of linkdate keys (for the Iterator interface implementation)
+ /**
+ * @var array List of all links IDS mapped with their array offset.
+ * Map: id->offset.
+ */
+ protected $ids;
+
+ // List of offset keys (for the Iterator interface implementation)
private $keys;
// Position in the $this->keys array (for the Iterator interface)
if (!$this->loggedIn) {
die('You are not authorized to add a link.');
}
- if (empty($value['linkdate']) || empty($value['url'])) {
- die('Internal Error: A link should always have a linkdate and URL.');
+ if (!isset($value['id']) || empty($value['url'])) {
+ die('Internal Error: A link should always have an id and URL.');
}
- if (empty($offset)) {
- die('You must specify a key.');
+ if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) {
+ die('You must specify an integer as a key.');
+ }
+ if (! empty($offset) && $offset !== $value['id']) {
+ die('Array offset and link ID must be equal.');
+ }
+
+ // If the link exists, we reuse the real offset, otherwise new entry
+ $existing = $this->getLinkOffset($offset);
+ if ($existing !== null) {
+ $offset = $existing;
+ } else {
+ $offset = count($this->links);
}
$this->links[$offset] = $value;
- $this->urls[$value['url']]=$offset;
+ $this->urls[$value['url']] = $offset;
+ $this->ids[$value['id']] = $offset;
}
/**
*/
public function offsetExists($offset)
{
- return array_key_exists($offset, $this->links);
+ return array_key_exists($this->getLinkOffset($offset), $this->links);
}
/**
// TODO: raise an exception
die('You are not authorized to delete a link.');
}
- $url = $this->links[$offset]['url'];
+ $realOffset = $this->getLinkOffset($offset);
+ $url = $this->links[$realOffset]['url'];
unset($this->urls[$url]);
- unset($this->links[$offset]);
+ unset($this->ids[$realOffset]);
+ unset($this->links[$realOffset]);
}
/**
*/
public function offsetGet($offset)
{
- return isset($this->links[$offset]) ? $this->links[$offset] : null;
+ $realOffset = $this->getLinkOffset($offset);
+ return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null;
}
/**
*/
public function current()
{
- return $this->links[$this->keys[$this->position]];
+ return $this[$this->keys[$this->position]];
}
/**
*/
public function rewind()
{
- $this->keys = array_keys($this->links);
- rsort($this->keys);
+ $this->keys = array_keys($this->ids);
$this->position = 0;
}
// Create a dummy database for example
$this->links = array();
$link = array(
+ 'id' => 1,
'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
'url'=>'https://github.com/shaarli/Shaarli/wiki',
'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
You use the community supported version of the original Shaarli project, by Sebastien Sauvage.',
'private'=>0,
- 'linkdate'=> date('Ymd_His'),
+ 'created'=> new DateTime(),
'tags'=>'opensource software'
);
- $this->links[$link['linkdate']] = $link;
+ $link['shorturl'] = link_small_hash($link['created'], $link['id']);
+ $this->links[1] = $link;
$link = array(
+ 'id' => 0,
'title'=>'My secret stuff... - Pastebin.com',
'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
'private'=>1,
- 'linkdate'=> date('Ymd_His', strtotime('-1 minute')),
- 'tags'=>'secretstuff'
+ 'created'=> new DateTime('1 minute ago'),
+ 'tags'=>'secretstuff',
);
- $this->links[$link['linkdate']] = $link;
+ $link['shorturl'] = link_small_hash($link['created'], $link['id']);
+ $this->links[0] = $link;
// Write database to disk
$this->write();
*/
private function read()
{
-
// Public links are hidden and user not logged in => nothing to show
if ($this->hidePublicLinks && !$this->loggedIn) {
$this->links = array();
strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
}
- // If user is not logged in, filter private links.
- if (!$this->loggedIn) {
- $toremove = array();
- foreach ($this->links as $link) {
- if ($link['private'] != 0) {
- $toremove[] = $link['linkdate'];
- }
- }
- foreach ($toremove as $linkdate) {
- unset($this->links[$linkdate]);
+ $toremove = array();
+ foreach ($this->links as $key => &$link) {
+ if (! $this->loggedIn && $link['private'] != 0) {
+ // Transition for not upgraded databases.
+ $toremove[] = $key;
+ continue;
}
- }
-
- $this->urls = array();
- foreach ($this->links as &$link) {
- // Keep the list of the mapping URLs-->linkdate up-to-date.
- $this->urls[$link['url']] = $link['linkdate'];
// Sanitize data fields.
sanitizeLink($link);
else {
$link['real_url'] = $link['url'];
}
+
+ // To be able to load links before running the update, and prepare the update
+ if (! isset($link['created'])) {
+ $link['id'] = $link['linkdate'];
+ $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
+ if (! empty($link['updated'])) {
+ $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
+ }
+ $link['shorturl'] = smallHash($link['linkdate']);
+ }
+ }
+
+ // If user is not logged in, filter private links.
+ foreach ($toremove as $offset) {
+ unset($this->links[$offset]);
}
+
+ $this->reorder();
}
/**
$request = '';
}
- $linkFilter = new LinkFilter($this->links);
+ $linkFilter = new LinkFilter($this);
return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
}
public function days()
{
$linkDays = array();
- foreach (array_keys($this->links) as $day) {
- $linkDays[substr($day, 0, 8)] = 0;
+ foreach ($this->links as $link) {
+ $linkDays[$link['created']->format('Ymd')] = 0;
}
$linkDays = array_keys($linkDays);
sort($linkDays);
return $linkDays;
}
+
+ /**
+ * 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->links, function($a, $b) use ($order) {
+ return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
+ });
+
+ $this->urls = array();
+ $this->ids = array();
+ foreach ($this->links as $key => $link) {
+ $this->urls[$link['url']] = $key;
+ $this->ids[$link['id']] = $key;
+ }
+ }
+
+ /**
+ * Return the next key for link 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;
+ }
+
+ /**
+ * Returns a link offset in links array from its unique ID.
+ *
+ * @param int $id Persistent ID of a link.
+ *
+ * @return int Real offset in local array, or null if doesn't exist.
+ */
+ protected function getLinkOffset($id)
+ {
+ if (isset($this->ids[$id])) {
+ return $this->ids[$id];
+ }
+ return null;
+ }
}
public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
/**
- * @var array all available links.
+ * @var LinkDB all available links.
*/
private $links;
/**
- * @param array $links initialization.
+ * @param LinkDB $links initialization.
*/
public function __construct($links)
{
private function noFilter($privateonly = false)
{
if (! $privateonly) {
- krsort($this->links);
return $this->links;
}
$out = array();
- foreach ($this->links as $value) {
+ foreach ($this->links as $key => $value) {
if ($value['private']) {
- $out[$value['linkdate']] = $value;
+ $out[$key] = $value;
}
}
- krsort($out);
return $out;
}
private function filterSmallHash($smallHash)
{
$filtered = array();
- foreach ($this->links as $l) {
- if ($smallHash == smallHash($l['linkdate'])) {
+ foreach ($this->links as $key => $l) {
+ if ($smallHash == $l['shorturl']) {
// Yes, this is ugly and slow
- $filtered[$l['linkdate']] = $l;
+ $filtered[$key] = $l;
return $filtered;
}
}
$keys = array('title', 'description', 'url', 'tags');
// Iterate over every stored link.
- foreach ($this->links as $link) {
+ foreach ($this->links as $id => $link) {
// ignore non private links when 'privatonly' is on.
if (! $link['private'] && $privateonly === true) {
}
if ($found) {
- $filtered[$link['linkdate']] = $link;
+ $filtered[$id] = $link;
}
}
- krsort($filtered);
return $filtered;
}
return $filtered;
}
- foreach ($this->links as $link) {
+ foreach ($this->links as $key => $link) {
// ignore non private links when 'privatonly' is on.
if (! $link['private'] && $privateonly === true) {
continue;
}
if ($found) {
- $filtered[$link['linkdate']] = $link;
+ $filtered[$key] = $link;
}
}
- krsort($filtered);
return $filtered;
}
}
$filtered = array();
- foreach ($this->links as $l) {
- if (startsWith($l['linkdate'], $day)) {
- $filtered[$l['linkdate']] = $l;
+ foreach ($this->links as $key => $l) {
+ if ($l['created']->format('Ymd') == $day) {
+ $filtered[$key] = $l;
}
}
- ksort($filtered);
- return $filtered;
+
+ // sort by date ASC
+ return array_reverse($filtered, true);
}
/**
function format_description($description, $redirector = '', $indexUrl = '') {
return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl)));
}
+
+/**
+ * Generate a small hash for a link.
+ *
+ * @param DateTime $date Link creation date.
+ * @param int $id Link ID.
+ *
+ * @return string the small hash generated from link data.
+ */
+function link_small_hash($date, $id)
+{
+ return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id);
+}
if ($link['private'] == 0 && $selection == 'private') {
continue;
}
- $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+ $date = $link['created'];
$link['timestamp'] = $date->getTimestamp();
$link['taglist'] = str_replace(' ', ',', $link['tags']);
return $status;
}
+ /**
+ * Imports Web bookmarks from an uploaded Netscape bookmark dump
+ *
+ * @param array $post Server $_POST parameters
+ * @param array $files Server $_FILES parameters
+ * @param LinkDB $linkDb Loaded LinkDB instance
+ * @param string $pagecache Page cache
+ *
+ * @return string Summary of the bookmark import status
+ */
+ public static function import($post, $files, $linkDb, $pagecache)
+ {
+ $filename = $files['filetoupload']['name'];
+ $filesize = $files['filetoupload']['size'];
+ $data = file_get_contents($files['filetoupload']['tmp_name']);
+
+ if (strpos($data, '<!DOCTYPE NETSCAPE-Bookmark-file-1>') === false) {
+ return self::importStatus($filename, $filesize);
+ }
+
+ // Overwrite existing links?
+ $overwrite = ! empty($post['overwrite']);
+
+ // Add tags to all imported links?
+ if (empty($post['default_tags'])) {
+ $defaultTags = array();
+ } else {
+ $defaultTags = preg_split(
+ '/[\s,]+/',
+ escape($post['default_tags'])
+ );
+ }
+
+ // links are imported as public by default
+ $defaultPrivacy = 0;
+
+ $parser = new NetscapeBookmarkParser(
+ true, // nested tag support
+ $defaultTags, // additional user-specified tags
+ strval(1 - $defaultPrivacy) // defaultPub = 1 - defaultPrivacy
+ );
+ $bookmarks = $parser->parseString($data);
+
+ $importCount = 0;
+ $overwriteCount = 0;
+ $skipCount = 0;
+
+ foreach ($bookmarks as $bkm) {
+ $private = $defaultPrivacy;
+ if (empty($post['privacy']) || $post['privacy'] == 'default') {
+ // use value from the imported file
+ $private = $bkm['pub'] == '1' ? 0 : 1;
+ } else if ($post['privacy'] == 'private') {
+ // all imported links are private
+ $private = 1;
+ } else if ($post['privacy'] == 'public') {
+ // all imported links are public
+ $private = 0;
+ }
+
+ $newLink = array(
+ 'title' => $bkm['title'],
+ 'url' => $bkm['uri'],
+ 'description' => $bkm['note'],
+ 'private' => $private,
+ 'tags' => $bkm['tags']
+ );
+
+ $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
+
+ if ($existingLink !== false) {
+ if ($overwrite === false) {
+ // Do not overwrite an existing link
+ $skipCount++;
+ continue;
+ }
+
+ // Overwrite an existing link, keep its date
+ $newLink['id'] = $existingLink['id'];
+ $newLink['created'] = $existingLink['created'];
+ $newLink['updated'] = new DateTime();
+ $linkDb[$existingLink['id']] = $newLink;
+ $importCount++;
+ $overwriteCount++;
+ continue;
+ }
+
+ // Add a new link - @ used for UNIX timestamps
+ $newLinkDate = new DateTime('@'.strval($bkm['time']));
+ $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ $newLink['created'] = $newLinkDate;
+ $newLink['id'] = $linkDb->getNextId();
+ $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
+ $linkDb[$newLink['id']] = $newLink;
+ $importCount++;
+ }
+
+ $linkDb->save($pagecache);
+ return self::importStatus(
+ $filename,
+ $filesize,
+ $importCount,
+ $overwriteCount,
+ $skipCount
+ );
+ }
+
+ /**
+ * Generates an import status summary
+ *
+ * @param string $filename name of the file to import
+ * @param int $filesize size of the file to import
+ * @param int $importCount how many links were imported
+ * @param int $overwriteCount how many links were overwritten
+ * @param int $skipCount how many links were skipped
+ *
+ * @return string Summary of the bookmark import status
+ */
+ private static function importStatus(
+ $filename,
+ $filesize,
+ $importCount=0,
+ $overwriteCount=0,
+ $skipCount=0
+ )
+ {
+ $status = 'File '.$filename.' ('.$filesize.' bytes) ';
+ if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
+ $status .= 'has an unknown file format. Nothing was imported.';
+ } else {
+ $status .= 'was successfully processed: '.$importCount.' links imported, ';
+ $status .= $overwriteCount.' links overwritten, ';
+ $status .= $skipCount.' links skipped.';
+ }
+ return $status;
+ }
+
/**
* Imports Web bookmarks from an uploaded Netscape bookmark dump
*
public function updateMethodRenameDashTags()
{
$linklist = $this->linkDB->filterSearch();
- foreach ($linklist as $link) {
+ foreach ($linklist as $key => $link) {
$link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
$link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
- $this->linkDB[$link['linkdate']] = $link;
+ $this->linkDB[$key] = $link;
}
$this->linkDB->save($this->conf->get('resource.page_cache'));
return true;
}
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;
+ }
}
/**
* - are NOT cryptographically secure (they CAN be forged)
*
* In Shaarli, they are used as a tinyurl-like link to individual entries,
- * e.g. smallHash('20111006_131924') --> yZH23w
+ * built once with the combination of the date and item ID.
+ * e.g. smallHash('20111006_131924' . 142) --> eaWxtQ
+ *
+ * @warning before v0.8.1, smallhashes were built only with the date,
+ * and their value has been preserved.
*
* @param string $text Create a hash from this text.
*
-Allow from none
-Deny from all
+<IfModule version_module>
+ <IfVersion >= 2.4>
+ Require all denied
+ </IfVersion>
+ <IfVersion < 2.4>
+ Allow from none
+ Deny from all
+ </IfVersion>
+</IfModule>
+
+<IfModule !version_module>
+ Require all denied
+</IfModule>
-Allow from none
-Deny from all
+<IfModule version_module>
+ <IfVersion >= 2.4>
+ Require all denied
+ </IfVersion>
+ <IfVersion < 2.4>
+ Allow from none
+ Deny from all
+ </IfVersion>
+</IfModule>
+
+<IfModule !version_module>
+ Require all denied
+</IfModule>
<ul>
<li><a href="https://github.com/kalvn/shaarli-plugin-autosave">autosave</a> by <a href="https://github.com/kalvn">@kalvn</a>: Automatically saves data when editing a link to avoid any loss in case of crash or unexpected shutdown.<a href=".html"></a></li>
<li><a href="https://github.com/ArthurHoaro/code-coloration">Code Coloration</a> by <a href="https://github.com/ArthurHoaro">@ArthurHoaro</a>: client side code syntax highlighter.<a href=".html"></a></li>
-<li><a href="https://github.com/alexisju/social">social</a> by <a href="https://github.com/alexisju">@alexisju</a>: share links to social networks.<a href=".html"></a></li>
+<li><a href="https://github.com/kalvn/shaarli-plugin-disqus">Disqus</a> by <a href="https://github.com/kalvn">@kalvn</a>: Adds Disqus comment system to your Shaarli.<a href=".html"></a></li>
<li><a href="https://github.com/NerosTie/emojione">emojione</a> by <a href="https://github.com/NerosTie">@NerosTie</a>: Add colorful emojis to your Shaarli.<a href=".html"></a></li>
<li><a href="https://github.com/ArthurHoaro/launch-plugin">launch</a> - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.<a href=".html"></a></li>
-<li><a href="https://github.com/kalvn/shaarli-plugin-disqus">Disqus</a> by <a href="https://github.com/kalvn">@kalvn</a>: Adds Disqus comment system to your Shaarli.<a href=".html"></a></li>
+<li><a href="https://github.com/alexisju/social">social</a> by <a href="https://github.com/alexisju">@alexisju</a>: share links to social networks.<a href=".html"></a></li>
+<li><a href="https://github.com/ArthurHoaro/shaarli2twitter">shaarli2twitter</a> by <a href="https://github.com/ArthurHoaro">@ArthurHoaro</a> - Automatically tweet your shared links from Shaarli<a href=".html"></a></li>
</ul>
<h3 id="themes">Themes</h3>
<p>See <a href="Theming.html">Theming</a> for the list of community-contributed themes, and an installation guide.</p>
<li><a href="https://github.com/DMeloni/shaarlo">Shaarlo</a> - An aggregator for shaarlis with many features (a very popular running instance among french shaarliers: <a href="http://shaarli.fr/">shaarli.fr</a>)<a href=".html"></a></li>
<li><a href="https://github.com/BoboTiG/shaarlimages">Shaarlimages</a> - An image-oriented aggregator for Shaarlis<a href=".html"></a></li>
<li><a href="https://github.com/mknexen/shaarli-api">mknexen/shaarli-api</a> - A REST API for Shaarli<a href=".html"></a></li>
-<li><a href="https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php">Self dead link</a> - Detect dead links on shaarli. This version use the database of shaarli. An <a href="https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php">another version</a>, can be used for others shaarli (but use most ressources).<a href=".html"></a></li>
+<li><a href="https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php">Self dead link</a> - Detect dead links on shaarli. This version use the database of shaarli. <a href="https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php">Another version</a>, can be used for other shaarli instances (but is more resource consuming).<a href=".html"></a></li>
</ul>
<h3 id="mobile-apps">Mobile Apps</h3>
<ul>
<ul>
<li><a href="https://github.com/jcsaaddupuy/tt-rss-shaarli">tt-rss-shaarli</a> - <a href="http://tt-rss.org/">TinyTiny RSS</a> plugin that adds support for sharing articles with Shaarli<a href=".html"></a></li>
<li><a href="https://github.com/ahmet2mir/octopress-shaarli">octopress-shaarli</a> - Octopress plugin to retrieve Shaarli links on the sidebar<a href=".html"></a></li>
+<li><a href="https://github.com/q2apro/scuttle-to-shaarli">Scuttle to Shaarli</a> - Import bookmarks from Scuttle<a href=".html"></a></li>
</ul>
<h2 id="alternatives-to-shaarli">Alternatives to Shaarli</h2>
<ul>
* [autosave](https://github.com/kalvn/shaarli-plugin-autosave) by [@kalvn](https://github.com/kalvn): Automatically saves data when editing a link to avoid any loss in case of crash or unexpected shutdown.[](.html)
* [Code Coloration](https://github.com/ArthurHoaro/code-coloration) by [@ArthurHoaro](https://github.com/ArthurHoaro): client side code syntax highlighter.[](.html)
- * [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.[](.html)
+ * [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.[](.html)
* [emojione](https://github.com/NerosTie/emojione) by [@NerosTie](https://github.com/NerosTie): Add colorful emojis to your Shaarli.[](.html)
* [launch](https://github.com/ArthurHoaro/launch-plugin) - Launch Plugin is a plugin designed to enhance and customize Launch Theme for Shaarli.[](.html)
- * [Disqus](https://github.com/kalvn/shaarli-plugin-disqus) by [@kalvn](https://github.com/kalvn): Adds Disqus comment system to your Shaarli.[](.html)
+ * [social](https://github.com/alexisju/social) by [@alexisju](https://github.com/alexisju): share links to social networks.[](.html)
+ * [shaarli2twitter](https://github.com/ArthurHoaro/shaarli2twitter) by [@ArthurHoaro](https://github.com/ArthurHoaro) - Automatically tweet your shared links from Shaarli[](.html)
### Themes
- [Shaarlo](https://github.com/DMeloni/shaarlo) - An aggregator for shaarlis with many features (a very popular running instance among french shaarliers: [shaarli.fr](http://shaarli.fr/))[](.html)
- [Shaarlimages](https://github.com/BoboTiG/shaarlimages) - An image-oriented aggregator for Shaarlis[](.html)
- [mknexen/shaarli-api](https://github.com/mknexen/shaarli-api) - A REST API for Shaarli[](.html)
-- [Self dead link](https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. An [another version](https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for others shaarli (but use most ressources).[](.html)
+- [Self dead link](https://github.com/qwertygc/shaarli-dev-code/blob/master/self-dead-link.php) - Detect dead links on shaarli. This version use the database of shaarli. [Another version](https://github.com/qwertygc/shaarli-dev-code/blob/master/dead-link.php), can be used for other shaarli instances (but is more resource consuming).[](.html)
### Mobile Apps
- [Shaarli💫](http://app.mro.name/Shaarli💫) iOS share extension - see [#308](https://github.com/shaarli/Shaarli/issues/308#issuecomment-184592070) for some promo codes,[](.html)
## Integration with other platforms
- [tt-rss-shaarli](https://github.com/jcsaaddupuy/tt-rss-shaarli) - [TinyTiny RSS](http://tt-rss.org/) plugin that adds support for sharing articles with Shaarli[](.html)
- [octopress-shaarli](https://github.com/ahmet2mir/octopress-shaarli) - Octopress plugin to retrieve Shaarli links on the sidebar[](.html)
+- [Scuttle to Shaarli](https://github.com/q2apro/scuttle-to-shaarli) - Import bookmarks from Scuttle[](.html)
## Alternatives to Shaarli
- [Shaarli alternatives](http://alternativeto.net/software/shaarli/) (alternativeto.net)[](.html)
<p>Several releases are available:</p>
<hr />
<h2 id="latest-release-recommended">Latest release (recommended)</h2>
-<p>Get the latest released version from the <a href="https://github.com/shaarli/Shaarli/releases">releases</a> page.<a href=".html"></a></p>
-<p>The current latest released version is <code>v0.7.0</code>.</p>
<h3 id="download-as-an-archive">Download as an archive</h3>
-<p>As a .zip archive:</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="fu">wget</span> https://github.com/shaarli/Shaarli/archive/v0.7.0.zip
-$ <span class="fu">unzip</span> Shaarli-v0.7.0.zip
-$ <span class="fu">mv</span> Shaarli-v0.7.0 /path/to/shaarli/</code></pre></div>
+<p>Get the latest released version from the <a href="https://github.com/shaarli/Shaarli/releases">releases</a> page.<a href=".html"></a></p>
+<p><strong>Download our <em>shaarli-full</em> archive</strong> to include dependencies.</p>
+<p>The current latest released version is <code>v0.8.0</code></p>
+<p>Or in command lines:</p>
+<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="fu">wget</span> https://github.com/shaarli/Shaarli/releases/download/v0.8.0/shaarli-v0.8.0-full.zip
+$ <span class="fu">unzip</span> shaarli-v0.8.0-full.zip
+$ <span class="fu">mv</span> Shaarli /path/to/shaarli/</code></pre></div>
<table style="width:46%;">
<colgroup>
<col style="width: 8%" />
<tbody>
</tbody>
</table>
+<h3 id="using-git">Using git</h3>
+<pre><code>mkdir -p /path/to/shaarli && cd /path/to/shaarli/
+git clone -b v0.8.0 https://github.com/shaarli/Shaarli.git .
+composer update --no-dev</code></pre>
<hr />
<h2 id="stable-version">Stable version</h2>
<p>The stable version has been experienced by Shaarli users, and will receive security updates.</p>
--------------------------------------------------------
## Latest release (recommended)
-
+### Download as an archive
Get the latest released version from the [releases](https://github.com/shaarli/Shaarli/releases) page.[](.html)
-The current latest released version is `v0.7.0`.
+**Download our *shaarli-full* archive** to include dependencies.
-### Download as an archive
+The current latest released version is `v0.8.0`
-As a .zip archive:
+Or in command lines:
```bash
-$ wget https://github.com/shaarli/Shaarli/archive/v0.7.0.zip
-$ unzip Shaarli-v0.7.0.zip
-$ mv Shaarli-v0.7.0 /path/to/shaarli/
+$ wget https://github.com/shaarli/Shaarli/releases/download/v0.8.0/shaarli-v0.8.0-full.zip
+$ unzip shaarli-v0.8.0-full.zip
+$ mv Shaarli /path/to/shaarli/
```
-
| ! |In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).|[](.html)
|-----|--------------------------|
+### Using git
+```
+mkdir -p /path/to/shaarli && cd /path/to/shaarli/
+git clone -b v0.8.0 https://github.com/shaarli/Shaarli.git .
+composer update --no-dev
+```
--------------------------------------------------------
<li><code>origin</code> pointing to your GitHub fork</li>
<li><code>upstream</code> pointing to the main Shaarli repository</li>
</ul></li>
-<li>maintainer permissions on the main Shaarli repository (to push the signed tag)</li>
+<li>maintainer permissions on the main Shaarli repository, to:
+<ul>
+<li>push the signed tag</li>
+<li>create a new release</li>
+</ul></li>
<li><a href="https://getcomposer.org/">Composer</a> and <a href="http://pandoc.org/">Pandoc</a> need to be installed<a href=".html"></a></li>
</ul>
+<h2 id="github-release-draft-and-changelog.md">GitHub release draft and <code>CHANGELOG.md</code></h2>
+<p>See <a href="http://keepachangelog.com/en/0.3.0/" class="uri">http://keepachangelog.com/en/0.3.0/</a> for changelog formatting.</p>
+<h3 id="github-release-draft">GitHub release draft</h3>
+<p>GitHub allows drafting the release note for the upcoming release, from the <a href="https://github.com/shaarli/Shaarli/releases">Releases</a> page. This way, the release note can be drafted while contributions are merged to <code>master</code>.<a href=".html"></a></p>
+<h3 id="changelog.md"><code>CHANGELOG.md</code></h3>
+<p>This file should contain the same information as the release note draft for the upcoming version.</p>
+<p>Update it to:</p>
+<ul>
+<li>add new entries (additions, fixes, etc.)</li>
+<li>mark the current version as released by setting its date and link</li>
+<li>add a new section for the future unreleased version</li>
+</ul>
+<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="bu">cd</span> /path/to/shaarli
+
+$ <span class="fu">nano</span> CHANGELOG.md
+
+[<span class="ex">...</span>][](.html)
+<span class="co">## vA.B.C - UNRELEASED</span>
+<span class="ex">TBA</span>
+
+<span class="co">## [vX.Y.Z](https://github.com/shaarli/Shaarli/releases/tag/vX.Y.Z) - YYYY-MM-DD[](.html)</span>
+[<span class="ex">...</span>][](.html)</code></pre></div>
<h2 id="increment-the-version-code-create-and-push-a-signed-tag">Increment the version code, create and push a signed tag</h2>
<h3 id="bump-shaarlis-version">Bump Shaarli's version</h3>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="bu">cd</span> /path/to/shaarli
$ <span class="fu">git</span> verify-tag f7762cf803f03f5caf4b8078359a63783d0090c1
<span class="ex">gpg</span>: Signature made Thu 30 Jul 2015 11:46:34 CEST using RSA key ID 4100DF6F
<span class="ex">gpg</span>: Good signature from <span class="st">"VirtualTam <virtualtam@flibidi.net>"</span> [ultimate][](.html)</code></pre></div>
-<h2 id="generate-and-upload-all-in-one-release-archives">Generate and upload all-in-one release archives</h2>
+<h2 id="publish-the-github-release">Publish the GitHub release</h2>
+<h3 id="create-a-github-release-from-a-git-tag">Create a GitHub release from a Git tag</h3>
+<p>From the previously drafted release:</p>
+<ul>
+<li>edit the release notes (if needed)</li>
+<li>specify the appropriate Git tag</li>
+<li>publish the release</li>
+<li>profit!</li>
+</ul>
+<h3 id="generate-and-upload-all-in-one-release-archives">Generate and upload all-in-one release archives</h3>
<p>Users with a shared hosting may have:</p>
<ul>
<li>no SSH access</li>
- a local clone of your Shaarli fork, with the following remotes:
- `origin` pointing to your GitHub fork
- `upstream` pointing to the main Shaarli repository
-- maintainer permissions on the main Shaarli repository (to push the signed tag)
+- maintainer permissions on the main Shaarli repository, to:
+ - push the signed tag
+ - create a new release
- [Composer](https://getcomposer.org/) and [Pandoc](http://pandoc.org/) need to be installed[](.html)
+## GitHub release draft and `CHANGELOG.md`
+See http://keepachangelog.com/en/0.3.0/ for changelog formatting.
+
+### GitHub release draft
+GitHub allows drafting the release note for the upcoming release, from the [Releases](https://github.com/shaarli/Shaarli/releases) page. This way, the release note can be drafted while contributions are merged to `master`.[](.html)
+
+### `CHANGELOG.md`
+This file should contain the same information as the release note draft for the upcoming version.
+
+Update it to:
+- add new entries (additions, fixes, etc.)
+- mark the current version as released by setting its date and link
+- add a new section for the future unreleased version
+
+```bash
+$ cd /path/to/shaarli
+
+$ nano CHANGELOG.md
+
+[...][](.html)
+## vA.B.C - UNRELEASED
+TBA
+
+## [vX.Y.Z](https://github.com/shaarli/Shaarli/releases/tag/vX.Y.Z) - YYYY-MM-DD[](.html)
+[...][](.html)
+```
+
+
## Increment the version code, create and push a signed tag
### Bump Shaarli's version
```bash
gpg: Good signature from "VirtualTam <virtualtam@flibidi.net>" [ultimate][](.html)
```
-## Generate and upload all-in-one release archives
+## Publish the GitHub release
+### Create a GitHub release from a Git tag
+From the previously drafted release:
+- edit the release notes (if needed)
+- specify the appropriate Git tag
+- publish the release
+- profit!
+
+### Generate and upload all-in-one release archives
Users with a shared hosting may have:
- no SSH access
- no possibility to install PHP packages or server extensions
ErrorLog<span class="st"> /var/log/apache2/shaarli-error.log</span>
CustomLog<span class="st"> /var/log/apache2/shaarli-access.log combined</span>
<span class="fu"></VirtualHost></span></code></pre></div>
+<h3 id="htaccess">.htaccess</h3>
+<p>Shaarli use <code>.htaccess</code> Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive <code>AllowOverride All</code> in your virtual host configuration for them to work.</p>
+<p><strong>Warning</strong>: If you use Apache 2.2 or lower, you need <a href="https://httpd.apache.org/docs/current/mod/mod_version.html">mod_version</a> to be installed and enabled.<a href=".html"></a></p>
<h2 id="lighthttpd">LightHttpd</h2>
<h2 id="nginx">Nginx</h2>
<h3 id="foreword">Foreword</h3>
<li>files may be located in a user's home directory</li>
<li>in this case, make sure both Nginx and PHP-FPM are running as the local user/group!</li>
</ul>
-<p>For all following examples, a development configuration will be used:</p>
+<p>For all following configuration examples, this user/group pair will be used:</p>
<ul>
<li><code>user:group = john:users</code>,</li>
</ul>
http {
[...][](.html)
}</code></pre>
+<h3 id="optional-increase-the-maximum-file-upload-size">(Optional) Increase the maximum file upload size</h3>
+<p>Some bookmark dumps generated by web browsers can be <em>huge</em> due to the presence of Base64-encoded images and favicons, as well as extra verbosity when nesting links in (sub-)folders.</p>
+<p>To increase upload size, you will need to modify both nginx and PHP configuration:</p>
+<pre class="nginx"><code># /etc/nginx/nginx.conf
+
+http {
+ [...][](.html)
+
+ client_max_body_size 10m;
+
+ [...][](.html)
+}</code></pre>
+<div class="sourceCode"><pre class="sourceCode ini"><code class="sourceCode ini"><span class="co"># /etc/php5/fpm/php.ini</span>
+
+<span class="kw">[...][]</span><span class="dt">(.html)</span>
+<span class="dt">post_max_size </span><span class="ot">=</span><span class="st"> 10M</span>
+<span class="kw">[...][]</span><span class="dt">(.html)</span>
+<span class="dt">upload_max_filesize </span><span class="ot">=</span><span class="st"> 10M</span></code></pre></div>
<h3 id="minimal-1">Minimal</h3>
<p><em>WARNING: Use for development only!</em></p>
<pre class="nginx"><code>user john users;
error_log /var/log/nginx/shaarli.error.log;
}
+ location = /shaarli/favicon.ico {
+ # serve the Shaarli favicon from its custom location
+ alias /var/www/shaarli/images/favicon.ico;
+ }
+
include deny.conf;
include static_assets.conf;
include php.conf;
error_log /var/log/nginx/shaarli.error.log;
}
+ location = /shaarli/favicon.ico {
+ # serve the Shaarli favicon from its custom location
+ alias /var/www/shaarli/images/favicon.ico;
+ }
+
include deny.conf;
include static_assets.conf;
include php.conf;
</VirtualHost>
```
+### .htaccess
+
+Shaarli use `.htaccess` Apache files to deny access to files that shouldn't be directly accessed (datastore, config, etc.). You need the directive `AllowOverride All` in your virtual host configuration for them to work.
+
+**Warning**: If you use Apache 2.2 or lower, you need [mod_version](https://httpd.apache.org/docs/current/mod/mod_version.html) to be installed and enabled.[](.html)
+
## LightHttpd
## Nginx
- files may be located in a user's home directory
- in this case, make sure both Nginx and PHP-FPM are running as the local user/group!
-For all following examples, a development configuration will be used:
+For all following configuration examples, this user/group pair will be used:
- `user:group = john:users`,
which corresponds to the following service configuration:
}
```
+### (Optional) Increase the maximum file upload size
+Some bookmark dumps generated by web browsers can be _huge_ due to the presence of Base64-encoded images and favicons, as well as extra verbosity when nesting links in (sub-)folders.
+
+To increase upload size, you will need to modify both nginx and PHP configuration:
+
+```nginx
+# /etc/nginx/nginx.conf
+
+http {
+ [...][](.html)
+
+ client_max_body_size 10m;
+
+ [...][](.html)
+}
+```
+
+```ini
+# /etc/php5/fpm/php.ini
+
+[...][](.html)
+post_max_size = 10M
+[...][](.html)
+upload_max_filesize = 10M
+```
+
### Minimal
_WARNING: Use for development only!_
error_log /var/log/nginx/shaarli.error.log;
}
+ location = /shaarli/favicon.ico {
+ # serve the Shaarli favicon from its custom location
+ alias /var/www/shaarli/images/favicon.ico;
+ }
+
include deny.conf;
include static_assets.conf;
include php.conf;
error_log /var/log/nginx/shaarli.error.log;
}
+ location = /shaarli/favicon.ico {
+ # serve the Shaarli favicon from its custom location
+ alias /var/www/shaarli/images/favicon.ico;
+ }
+
include deny.conf;
include static_assets.conf;
include php.conf;
<ul>
<li>There should now be a <code>my-template/</code> directory under the <code>tpl/</code> dir, containing directly all the template files.</li>
</ul></li>
-<li><p>Edit <code>data/config.php</code> to have Shaarli use this template, e.g.</p>
-<div class="sourceCode"><pre class="sourceCode php"><code class="sourceCode php"><span class="kw">$GLOBALS</span><span class="ot">[</span><span class="st">'config'</span><span class="ot">[</span><span class="st">'RAINTPL_TPL'</span><span class="ot">]</span> = <span class="st">'tpl/my-template/'</span><span class="ot">;](</span><span class="st">'RAINTPL_TPL'</span><span class="ot">]</span>-=-<span class="st">'tpl/my-template/'</span><span class="ot">;</span>.html<span class="ot">)</span></code></pre></div></li>
+<li><p>Edit <code>data/config.json.php</code> to have Shaarli use this template, in <code>"resource"</code> e.g.</p>
+<div class="sourceCode"><pre class="sourceCode json"><code class="sourceCode json"><span class="er">"raintpl_tpl":</span> <span class="er">"tpl\/my-template\/",</span></code></pre></div></li>
</ul>
<h2 id="community-themes-templates">Community themes & templates</h2>
<ul>
<li><a href="https://github.com/AkibaTech/Shaarli---SuperHero-Theme">AkibaTech/Shaarli Superhero Theme</a> - A template/theme for Shaarli<a href=".html"></a></li>
<li><a href="https://github.com/alexisju/albinomouse-template">alexisju/albinomouse-template</a> - A full template for Shaarli<a href=".html"></a></li>
+<li><a href="https://github.com/ArthurHoaro/shaarli-launch">ArthurHoaro/shaarli-launch</a> - Customizable Shaarli theme.<a href=".html"></a></li>
<li><a href="https://github.com/dhoko/ShaarliTemplate">dhoko/ShaarliTemplate</a> - A template/theme for Shaarli<a href=".html"></a></li>
<li><a href="https://github.com/kalvn/shaarli-blocks">kalvn/shaarli-blocks</a> - A template/theme for Shaarli<a href=".html"></a></li>
<li><a href="https://github.com/kalvn/Shaarli-Material">kalvn/Shaarli-Material</a> - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone.<a href=".html"></a></li>
+<li><a href="https://github.com/ManufacturaInd/shaarli-2004licious-theme">ManufacturaInd/shaarli-2004licious-theme</a> - A template/theme as a humble homage to the early looks of the del.icio.us site.<a href=".html"></a></li>
<li><a href="https://github.com/misterair/limonade">misterair/Limonade</a> - A fork of (legacy) Shaarli with a new template<a href=".html"></a></li>
<li><a href="https://github.com/mrjovanovic/serious-theme-shaarli">mrjovanovic/serious-theme-shaarli</a> - A serious theme for SHaarli.<a href=".html"></a></li>
-<li><a href="https://github.com/Vinm/Blue-theme-for-Shaarli">Vinm/Blue-theme-for Shaarli</a> - A template/theme for Shaarli (<a href="https://github.com/Vinm/Blue-theme-for-Shaarli/issues/2">unmaintained</a>, compatibility unknown)<a href=".html"></a></li>
<li><a href="https://github.com/vivienhaese/shaarlitheme">vivienhaese/shaarlitheme</a> - A Shaarli fork meant to be run in an openshift instance<a href=".html"></a></li>
</ul>
<h3 id="example-installation-albinomouse-template">Example installation: AlbinoMouse template</h3>
- Find it's git clone URL or download the zip archive for the template.
- In your Shaarli `tpl/` directory, run `git clone https://url/of/my-template/` or unpack the zip archive.
- There should now be a `my-template/` directory under the `tpl/` dir, containing directly all the template files.
-- Edit `data/config.php` to have Shaarli use this template, e.g.
-```php
-$GLOBALS['config'['RAINTPL_TPL'] = 'tpl/my-template/';]('RAINTPL_TPL']-=-'tpl/my-template/';.html)
+- Edit `data/config.json.php` to have Shaarli use this template, in `"resource"` e.g.
+```json
+"raintpl_tpl": "tpl\/my-template\/",
```
## Community themes & templates
- [AkibaTech/Shaarli Superhero Theme](https://github.com/AkibaTech/Shaarli---SuperHero-Theme) - A template/theme for Shaarli[](.html)
- [alexisju/albinomouse-template](https://github.com/alexisju/albinomouse-template) - A full template for Shaarli[](.html)
+- [ArthurHoaro/shaarli-launch](https://github.com/ArthurHoaro/shaarli-launch) - Customizable Shaarli theme.[](.html)
- [dhoko/ShaarliTemplate](https://github.com/dhoko/ShaarliTemplate) - A template/theme for Shaarli[](.html)
- [kalvn/shaarli-blocks](https://github.com/kalvn/shaarli-blocks) - A template/theme for Shaarli[](.html)
- [kalvn/Shaarli-Material](https://github.com/kalvn/Shaarli-Material) - A theme (template) based on Google's Material Design for Shaarli, the superfast delicious clone.[](.html)
+- [ManufacturaInd/shaarli-2004licious-theme](https://github.com/ManufacturaInd/shaarli-2004licious-theme) - A template/theme as a humble homage to the early looks of the del.icio.us site.[](.html)
- [misterair/Limonade](https://github.com/misterair/limonade) - A fork of (legacy) Shaarli with a new template[](.html)
- [mrjovanovic/serious-theme-shaarli](https://github.com/mrjovanovic/serious-theme-shaarli) - A serious theme for SHaarli.[](.html)
-- [Vinm/Blue-theme-for Shaarli](https://github.com/Vinm/Blue-theme-for-Shaarli) - A template/theme for Shaarli ([unmaintained](https://github.com/Vinm/Blue-theme-for-Shaarli/issues/2), compatibility unknown)[](.html)
- [vivienhaese/shaarlitheme](https://github.com/vivienhaese/shaarlitheme) - A Shaarli fork meant to be run in an openshift instance[](.html)
### Example installation: AlbinoMouse template
-Allow from none
-Deny from all
+<IfModule version_module>
+ <IfVersion >= 2.4>
+ Require all denied
+ </IfVersion>
+ <IfVersion < 2.4>
+ Allow from none
+ Deny from all
+ </IfVersion>
+</IfModule>
+
+<IfModule !version_module>
+ Require all denied
+</IfModule>
nano \
&& apt-get clean
+RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
+RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
COPY nginx.conf /etc/nginx/nginx.conf
COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
default_type application/octet-stream;
keepalive_timeout 20;
+ client_max_body_size 10m;
+
index index.html index.php;
server {
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
}
+ location = /favicon.ico {
+ # serve the Shaarli favicon from its custom location
+ alias /var/www/shaarli/images/favicon.ico;
+ }
+
location ~ (index)\.php$ {
# filter and proxy PHP requests to PHP-FPM
fastcgi_pass unix:/var/run/php5-fpm.sock;
supervisor \
&& apt-get clean
+RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
+RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
COPY nginx.conf /etc/nginx/nginx.conf
COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
default_type application/octet-stream;
keepalive_timeout 20;
+ client_max_body_size 10m;
+
index index.html index.php;
server {
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
}
+ location = /favicon.ico {
+ # serve the Shaarli favicon from its custom location
+ alias /var/www/shaarli/images/favicon.ico;
+ }
+
location ~ (index)\.php$ {
# filter and proxy PHP requests to PHP-FPM
fastcgi_pass unix:/var/run/php5-fpm.sock;
supervisor \
&& apt-get clean
+RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
+RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
COPY nginx.conf /etc/nginx/nginx.conf
COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
default_type application/octet-stream;
keepalive_timeout 20;
+ client_max_body_size 10m;
+
index index.html index.php;
server {
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
}
+ location = /favicon.ico {
+ # serve the Shaarli favicon from its custom location
+ alias /var/www/shaarli/images/favicon.ico;
+ }
+
location ~ (index)\.php$ {
# filter and proxy PHP requests to PHP-FPM
fastcgi_pass unix:/var/run/php5-fpm.sock;
font-style: italic;
}
+strong {
+ font-weight: bold;
+}
+
/* Buttons */
.bigbutton {
background-color: #c0c0c0;
}
#pluginsadmin a {
+ color: #486D08;
+}
+
+#pluginsadmin a.arrow {
color: black;
}
+
/* 404 page */
.error-container {
<?php
/**
- * Shaarli v0.8.0 - Shaare your links...
+ * Shaarli v0.8.1 - Shaare your links...
*
* The personal, minimalist, super-fast, database free, bookmarking service.
*
/*
* PHP configuration
*/
-define('shaarli_version', '0.8.0');
+define('shaarli_version', '0.8.1');
// http://server.com/x/shaarli --> /shaarli/
define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
);
/* Some Shaarlies may have very few links, so we need to look
- back in time (rsort()) until we have enough days ($nb_of_days).
+ back in time until we have enough days ($nb_of_days).
*/
- $linkdates = array();
- foreach ($LINKSDB as $linkdate => $value) {
- $linkdates[] = $linkdate;
- }
- rsort($linkdates);
$nb_of_days = 7; // We take 7 days.
$today = date('Ymd');
$days = array();
- foreach ($linkdates as $linkdate) {
- $day = substr($linkdate, 0, 8); // Extract day (without time)
- if (strcmp($day,$today) < 0) {
+ foreach ($LINKSDB as $link) {
+ $day = $link['created']->format('Ymd'); // Extract day (without time)
+ if (strcmp($day, $today) < 0) {
if (empty($days[$day])) {
$days[$day] = array();
}
- $days[$day][] = $linkdate;
+ $days[$day][] = $link;
}
if (count($days) > $nb_of_days) {
echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
// For each day.
- foreach ($days as $day => $linkdates) {
+ foreach ($days as $day => $links) {
$dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
$absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
- // Build the HTML body of this RSS entry.
- $links = array();
-
// We pre-format some fields for proper output.
- foreach ($linkdates as $linkdate) {
- $l = $LINKSDB[$linkdate];
- $l['formatedDescription'] = format_description($l['description'], $conf->get('redirector.url'));
- $l['thumbnail'] = thumbnail($conf, $l['url']);
- $l_date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $l['linkdate']);
- $l['timestamp'] = $l_date->getTimestamp();
- if (startsWith($l['url'], '?')) {
- $l['url'] = index_url($_SERVER) . $l['url']; // make permalink URL absolute
+ foreach ($links as &$link) {
+ $link['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
+ $link['thumbnail'] = thumbnail($conf, $link['url']);
+ $link['timestamp'] = $link['created']->getTimestamp();
+ if (startsWith($link['url'], '?')) {
+ $link['url'] = index_url($_SERVER) . $link['url']; // make permalink URL absolute
}
- $links[$linkdate] = $l;
}
// Then build the HTML for this day:
$linksToDisplay[$key]['taglist']=$taglist;
$linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
$linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']);
- $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
- $linksToDisplay[$key]['timestamp'] = $date->getTimestamp();
+ $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
}
/* We need to spread the articles on 3 columns.
// Get only links which have a thumbnail.
foreach($links as $link)
{
- $permalink='?'.escape(smallHash($link['linkdate']));
+ $permalink='?'.$link['shorturl'];
$thumb=lazyThumbnail($conf, $link['url'],$permalink);
if ($thumb!='') // Only output links which have a thumbnail.
{
{
$data = array(
'pageabsaddr' => index_url($_SERVER),
+ 'sslenabled' => !empty($_SERVER['HTTPS'])
);
$pluginManager->executeHooks('render_tools', $data);
// -------- User clicked the "Save" button when editing a link: Save link to database.
if (isset($_POST['save_edit']))
{
- $linkdate = $_POST['lf_linkdate'];
- $updated = isset($LINKSDB[$linkdate]) ? strval(date('Ymd_His')) : false;
-
// Go away!
if (! tokenOk($_POST['token'])) {
die('Wrong token.');
}
+
+ // lf_id should only be present if the link exists.
+ $id = !empty($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
+ // Linkdate is kept here to:
+ // - use the same permalink for notes as they're displayed when creating them
+ // - let users hack creation date of their posts
+ // See: https://github.com/shaarli/Shaarli/wiki/Datastore-hacks#changing-the-timestamp-for-a-link
+ $linkdate = escape($_POST['lf_linkdate']);
+ if (isset($LINKSDB[$id])) {
+ // Edit
+ $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
+ $updated = new DateTime();
+ $shortUrl = $LINKSDB[$id]['shorturl'];
+ } else {
+ // New link
+ $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
+ $updated = null;
+ $shortUrl = link_small_hash($created, $id);
+ }
+
// Remove multiple spaces.
$tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
// Remove first '-' char in tags.
}
$link = array(
+ 'id' => $id,
'title' => trim($_POST['lf_title']),
'url' => $url,
'description' => $_POST['lf_description'],
'private' => (isset($_POST['lf_private']) ? 1 : 0),
- 'linkdate' => $linkdate,
+ 'created' => $created,
'updated' => $updated,
- 'tags' => str_replace(',', ' ', $tags)
+ 'tags' => str_replace(',', ' ', $tags),
+ 'shorturl' => $shortUrl,
);
+
// If title is empty, use the URL as title.
if ($link['title'] == '') {
$link['title'] = $link['url'];
$pluginManager->executeHooks('save_link', $link);
- $LINKSDB[$linkdate] = $link;
+ $LINKSDB[$id] = $link;
$LINKSDB->save($conf->get('resource.page_cache'));
pubsubhub($conf);
$returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
$location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
// Scroll to the link which has been edited.
- $location .= '#' . smallHash($_POST['lf_linkdate']);
+ $location .= '#' . $link['shorturl'];
// After saving the link, redirect to the page the user was on.
header('Location: '. $location);
exit;
{
// If we are called from the bookmarklet, we must close the popup:
if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
+ $link = $LINKSDB[(int) escape($_POST['lf_id'])];
$returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
- $returnurl .= '#'.smallHash($_POST['lf_linkdate']); // Scroll to the link which has been edited.
+ // Scroll to the link which has been edited.
+ $returnurl .= '#'. $link['shorturl'];
$returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
exit;
}
// -------- User clicked the "Delete" button when editing a link: Delete link from database.
- if ($targetPage == Router::$PAGE_DELETELINK)
+ if (isset($_POST['delete_link']))
{
- if (!tokenOk($_GET['token'])) die('Wrong token.');
+ if (!tokenOk($_POST['token'])) die('Wrong token.');
+
// We do not need to ask for confirmation:
// - confirmation is handled by JavaScript
// - we are protected from XSRF by the token.
- $linkdate = $_GET['delete_link'];
- $link = $LINKSDB[$linkdate];
-
- $pluginManager->executeHooks('delete_link', $link);
- unset($LINKSDB[$linkdate]);
- $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
+ // FIXME! We keep `lf_linkdate` for consistency before a proper API. To be removed.
+ $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : intval(escape($_POST['lf_linkdate']));
+
+ $pluginManager->executeHooks('delete_link', $LINKSDB[$id]);
+
+ unset($LINKSDB[$id]);
+ $LINKSDB->save('resource.page_cache'); // save to disk
// If we are called from the bookmarklet, we must close the popup:
if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
// -------- User clicked the "EDIT" button on a link: Display link edit form.
if (isset($_GET['edit_link']))
{
- $link = $LINKSDB[$_GET['edit_link']]; // Read database
+ $id = (int) escape($_GET['edit_link']);
+ $link = $LINKSDB[$id]; // Read database
if (!$link) { header('Location: ?'); exit; } // Link not found in database.
+ $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
$data = array(
'link' => $link,
'link_is_new' => false,
$link_is_new = false;
// Check if URL is not already in database (in this case, we will edit the existing link)
$link = $LINKSDB->getLinkFromUrl($url);
- if (!$link)
+ if (! $link)
{
$link_is_new = true;
- $linkdate = strval(date('Ymd_His'));
+ $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT));
// Get title if it was provided in URL (by the bookmarklet).
$title = empty($_GET['title']) ? '' : escape($_GET['title']);
// Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
}
if ($url == '') {
- $url = '?' . smallHash($linkdate);
+ $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
$title = 'Note: ';
}
$url = escape($url);
'tags' => $tags,
'private' => $private
);
+ } else {
+ $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
}
$data = array(
$link['description'] = format_description($link['description'], $conf->get('redirector.url'));
$classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
$link['class'] = $link['private'] == 0 ? $classLi : 'private';
- $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
- $link['timestamp'] = $date->getTimestamp();
+ $link['timestamp'] = $link['created']->getTimestamp();
if (! empty($link['updated'])) {
- $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['updated']);
- $link['updated_timestamp'] = $date->getTimestamp();
+ $link['updated_timestamp'] = $link['updated']->getTimestamp();
} else {
$link['updated_timestamp'] = '';
}
$taglist = explode(' ', $link['tags']);
uasort($taglist, 'strcasecmp');
$link['taglist'] = $taglist;
- $link['shorturl'] = smallHash($link['linkdate']);
// Check for both signs of a note: starting with ? and 7 chars long.
if ($link['url'][0] === '?' &&
strlen($link['url']) === 7) {
-Allow from none
-Deny from all
+<IfModule version_module>
+ <IfVersion >= 2.4>
+ Require all denied
+ </IfVersion>
+ <IfVersion < 2.4>
+ Allow from none
+ Deny from all
+ </IfVersion>
+</IfModule>
+
+<IfModule !version_module>
+ Require all denied
+</IfModule>
+++ /dev/null
-#addlink_toolbar {
- display: inline;
- margin: 0 0 0 25px;
-}
\ No newline at end of file
{
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST && $data['_LOGGEDIN_'] === true) {
$form = array(
- 'method' => 'GET',
- 'action' => '',
- 'name' => 'addform',
- 'class' => 'addform',
+ 'attr' => array(
+ 'method' => 'GET',
+ 'action' => '',
+ 'name' => 'addform',
+ 'class' => 'addform',
+ ),
'inputs' => array(
array(
'type' => 'text',
-<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" /></a></span>
+<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" alt="archive.org" /></a></span>
* and a mandatory `html` key, which contains its value.
*/
$button = array(
- 'href' => '#',
- 'class' => 'mybutton',
- 'title' => 'hover me',
+ 'attr' => array (
+ 'href' => '#',
+ 'class' => 'mybutton',
+ 'title' => 'hover me',
+ ),
'html' => 'DEMO buttons toolbar',
);
$data['buttons_toolbar'][] = $button;
* </form>
*/
$form = array(
- 'method' => 'GET',
- 'action' => '?',
- 'class' => 'addform',
+ 'attr' => array(
+ 'method' => 'GET',
+ 'action' => '?',
+ 'class' => 'addform',
+ ),
'inputs' => array(
array(
'type' => 'text',
}
// Another button always displayed
$button = array(
- 'href' => '#',
+ 'attr' => array(
+ 'href' => '#',
+ ),
'html' => 'Demo',
);
$data['buttons_toolbar'][] = $button;
* It's also recommended to add key 'on' or 'off' for theme rendering.
*/
$action = array(
- 'href' => '?up',
- 'title' => 'Uppercase!',
+ 'attr' => array(
+ 'href' => '?up',
+ 'title' => 'Uppercase!',
+ ),
'html' => '←',
);
// Only display comments for permalinks.
if (count($data['links']) == 1 && empty($data['search_tags']) && empty($data['search_term'])) {
$link = reset($data['links']);
- $isso_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/isso/isso.html');
+ $issoHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/isso/isso.html');
- $isso = sprintf($isso_html, $issoUrl, $issoUrl, $link['linkdate'], $link['linkdate']);
+ $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']);
$data['plugin_end_zone'][] = $isso;
// Hackish way to include this CSS file only when necessary.
|--- markdown.css
|--- markdown.meta
|--- markdown.php
- |--- Parsedown.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.php`
-(`ENABLED_PLUGINS` array).
+You can also add `markdown` to your list of enabled plugins in `data/config.json.php`
+(`general.enabled_plugins` list).
This should look like:
```
-$GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode', 'any_other_plugin', 'markdown')
+"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.
+If the tag `nomarkdown` is set for a shaare, it won't be converted to Markdown syntax.
-> Note: it's a private tag (leading dot), so it won't be displayed to visitors.
+> Note: this is a special tag, so it won't be displayed in link list.
+
+### HTML rendering
+
+Markdown support HTML tags. For example:
+
+ > <strong>strong</strong><strike>strike</strike>
+
+Will render as:
+
+> <strong>strong</strong><strike>strike</strike>
+
+If you want to shaare HTML code, it is necessary to use inline code or code blocks.
+
+**If your shaared descriptions containing HTML tags before enabling the markdown plugin,
+enabling it might break your page.**
+
+> Note: HTML tags such as script, iframe, etc. are disabled for security reasons.
### Known issue
-description="Render shaare description with Markdown syntax."
+description="Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
+If your shaared descriptions containing 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>."
{
foreach ($data['links'] as &$value) {
if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
- $value['taglist'] = stripNoMarkdownTag($value['taglist']);
+ $value = stripNoMarkdownTag($value);
continue;
}
$value['description'] = process_markdown($value['description']);
{
foreach ($data['links'] as &$value) {
if (!empty($value['tags']) && noMarkdownTag($value['tags'])) {
- $value['tags'] = stripNoMarkdownTag($value['tags']);
+ $value = stripNoMarkdownTag($value);
continue;
}
$value['description'] = process_markdown($value['description']);
foreach ($data['cols'] as &$value) {
foreach ($value as &$value2) {
if (!empty($value2['tags']) && noMarkdownTag($value2['tags'])) {
+ $value2 = stripNoMarkdownTag($value2);
continue;
}
$value2['formatedDescription'] = process_markdown($value2['formatedDescription']);
*/
function noMarkdownTag($tags)
{
- return strpos($tags, NO_MD_TAG) !== false;
+ return preg_match('/(^|\s)'. NO_MD_TAG .'(\s|$)/', $tags);
}
/**
* Remove the no-markdown meta tag so it won't be displayed.
*
- * @param string $tags Tag list.
+ * @param array $link Link data.
*
- * @return string tag list without no markdown tag.
+ * @return array Updated link without no markdown tag.
*/
-function stripNoMarkdownTag($tags)
+function stripNoMarkdownTag($link)
{
- unset($tags[array_search(NO_MD_TAG, $tags)]);
- return array_values($tags);
+ 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;
}
/**
{
if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
$playvideo = array(
- 'href' => '#',
- 'title' => 'Video player',
- 'id' => 'playvideos',
+ 'attr' => array(
+ 'href' => '#',
+ 'title' => 'Video player',
+ 'id' => 'playvideos',
+ ),
'html' => '► Play Videos'
);
$data['buttons_toolbar'][] = $playvideo;
<div class="linkqrcode">
<a href="http://qrfree.kaywa.com/?l=1&s=8&d=%s" onclick="showQrCode(this); return false;" class="qrcode" data-permalink="%s">
- <img src="%s/qrcode/qrcode.png" class="linklist-plugin-icon" title="QR-Code">
+ <img src="%s/qrcode/qrcode.png" class="linklist-plugin-icon" title="QR-Code" alt="QRCode">
</a>
</div>
-<span><a href="%s?url=%s"><img class="linklist-plugin-icon" src="%s/readityourself/book-open.png" title="Read with Readityourself" /></a></span>
+<span><a href="%s?url=%s"><img class="linklist-plugin-icon" src="%s/readityourself/book-open.png" title="Read with Readityourself" alt="readityourself" /></a></span>
-<span><a href="%s%s" target="_blank"><img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="Save to wallabag" /></a></span>
+<span><a href="%s%s" target="_blank"><img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="Save to wallabag" alt="wallabag" /></a></span>
-<?php /* 0.8.0 */ ?>
+<?php /* 0.8.1 */ ?>
-Allow from none
-Deny from all
+<IfModule version_module>
+ <IfVersion >= 2.4>
+ Require all denied
+ </IfVersion>
+ <IfVersion < 2.4>
+ Allow from none
+ Deny from all
+ </IfVersion>
+</IfModule>
+
+<IfModule !version_module>
+ Require all denied
+</IfModule>
$this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
// Test first link (note link)
- $link = array_shift($data['links']);
- $this->assertEquals('20150310_114651', $link['linkdate']);
+ $link = reset($data['links']);
+ $this->assertEquals(41, $link['id']);
+ $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
$this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
$this->assertEquals('http://host.tld/?WDWyig', $link['url']);
$this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
$this->assertEquals('sTuff', $link['taglist'][0]);
// Test URL with external link.
- $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $data['links']['20150310_114633']['url']);
+ $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $data['links'][8]['url']);
// Test multitags.
- $this->assertEquals(5, count($data['links']['20141125_084734']['taglist']));
- $this->assertEquals('css', $data['links']['20141125_084734']['taglist'][0]);
+ $this->assertEquals(5, count($data['links'][6]['taglist']));
+ $this->assertEquals('css', $data['links'][6]['taglist'][0]);
// Test update date
- $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links']['20150310_114633']['up_iso_date']);
+ $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']);
}
/**
$data = $feedBuilder->buildData();
$this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
$this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']);
- $link = array_shift($data['links']);
+ $link = reset($data['links']);
$this->assertRegExp('/2015-03-10T11:46:51\+\d{2}:\d{2}/', $link['pub_iso_date']);
- $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links']['20150310_114633']['up_iso_date']);
+ $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']);
}
/**
$data = $feedBuilder->buildData();
$this->assertEquals(1, count($data['links']));
$link = array_shift($data['links']);
- $this->assertEquals('20150310_114651', $link['linkdate']);
+ $this->assertEquals(41, $link['id']);
+ $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
}
/**
$data = $feedBuilder->buildData();
$this->assertEquals(1, count($data['links']));
$link = array_shift($data['links']);
- $this->assertEquals('20150310_114651', $link['linkdate']);
+ $this->assertEquals(41, $link['id']);
+ $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
}
/**
$this->assertTrue($data['usepermalinks']);
// First link is a permalink
$link = array_shift($data['links']);
- $this->assertEquals('20150310_114651', $link['linkdate']);
+ $this->assertEquals(41, $link['id']);
+ $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
$this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
$this->assertEquals('http://host.tld/?WDWyig', $link['url']);
$this->assertContains('Direct link', $link['description']);
$this->assertContains('http://host.tld/?WDWyig', $link['description']);
// Second link is a direct link
$link = array_shift($data['links']);
- $this->assertEquals('20150310_114633', $link['linkdate']);
- $this->assertEquals('http://host.tld/?kLHmZg', $link['guid']);
+ $this->assertEquals(8, $link['id']);
+ $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
+ $this->assertEquals('http://host.tld/?RttfEw', $link['guid']);
$this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
$this->assertContains('Direct link', $link['description']);
$this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);
$dbSize = sizeof($testDB);
$link = array(
+ 'id' => 42,
'title'=>'an additional link',
'url'=>'http://dum.my',
'description'=>'One more',
'private'=>0,
- 'linkdate'=>'20150518_190000',
+ 'created'=> DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150518_190000'),
'tags'=>'unit test'
);
- $testDB[$link['linkdate']] = $link;
+ $testDB[$link['id']] = $link;
$testDB->save('tests');
$testDB = new LinkDB(self::$testDatastore, true, false);
public function testDays()
{
$this->assertEquals(
- array('20121206', '20130614', '20150310'),
+ array('20100310', '20121206', '20130614', '20150310'),
self::$publicLinkDB->days()
);
$this->assertEquals(
- array('20121206', '20130614', '20141125', '20150310'),
+ array('20100310', '20121206', '20130614', '20141125', '20150310'),
self::$privateLinkDB->days()
);
}
'stallman' => 1,
'free' => 1,
'-exclude' => 1,
+ 'hashtag' => 2,
// The DB contains a link with `sTuff` and another one with `stuff` tag.
- // They need to be grouped with the first case found (`sTuff`).
+ // They need to be grouped with the first case found - order by date DESC: `sTuff`.
'sTuff' => 2,
- 'hashtag' => 2,
+ 'ut' => 1,
),
self::$publicLinkDB->allTags()
);
'tag2' => 1,
'tag3' => 1,
'tag4' => 1,
+ 'ut' => 1,
),
self::$privateLinkDB->allTags()
);
1,
count(self::$publicLinkDB->filterHash($request))
);
+ $request = smallHash('20150310_114633' . 8);
+ $this->assertEquals(
+ 1,
+ count(self::$publicLinkDB->filterHash($request))
+ );
}
/**
{
self::$publicLinkDB->filterHash('');
}
+
+ /**
+ * Test reorder with asc/desc parameter.
+ */
+ public function testReorderLinksDesc()
+ {
+ self::$privateLinkDB->reorder('ASC');
+ $linkIds = array(42, 4, 1, 0, 7, 6, 8, 41);
+ $cpt = 0;
+ foreach (self::$privateLinkDB as $key => $value) {
+ $this->assertEquals($linkIds[$cpt++], $key);
+ }
+ self::$privateLinkDB->reorder('DESC');
+ $linkIds = array_reverse($linkIds);
+ $cpt = 0;
+ foreach (self::$privateLinkDB as $key => $value) {
+ $this->assertEquals($linkIds[$cpt++], $key);
+ }
+ }
}
$this->assertEquals(
'MediaGoblin',
- $links['20130614_184135']['title']
+ $links[7]['title']
);
}
);
$this->assertEquals(
- 6,
+ 7,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution'))
);
}
);
$this->assertEquals(
- 6,
+ 7,
count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free'))
);
}
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all', false, '');
$this->assertEquals(self::$refDb->countLinks(), sizeof($links));
foreach ($links as $link) {
- $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+ $date = $link['created'];
$this->assertEquals(
$date->getTimestamp(),
$link['timestamp']
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private', false, '');
$this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
foreach ($links as $link) {
- $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+ $date = $link['created'];
$this->assertEquals(
$date->getTimestamp(),
$link['timestamp']
$links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, '');
$this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
foreach ($links as $link) {
- $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+ $date = $link['created'];
$this->assertEquals(
$date->getTimestamp(),
$link['timestamp']
*/
protected $pagecache = 'tests';
+ /**
+ * @var string Save the current timezone.
+ */
+ protected static $defaultTimeZone;
+
+ public static function setUpBeforeClass()
+ {
+ self::$defaultTimeZone = date_default_timezone_get();
+ // Timezone without DST for test consistency
+ date_default_timezone_set('Africa/Nairobi');
+ }
+
/**
* Resets test data before each test
*/
$this->linkDb = new LinkDB(self::$testDatastore, true, false);
}
+ public static function tearDownAfterClass()
+ {
+ date_default_timezone_set(self::$defaultTimeZone);
+ }
+
/**
* Attempt to import bookmarks from an empty file
*/
$this->assertEquals(
array(
- 'linkdate' => '20160618_173944',
+ 'id' => 0,
+ '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' => ''
+ 'tags' => '',
+ 'shorturl' => 'La37cg',
),
$this->linkDb->getLinkFromUrl('http://hginit.com/')
);
}
-
/**
* Import bookmarks nested in a folder hierarchy
*/
$this->assertEquals(
array(
- 'linkdate' => '20160225_205541',
+ 'id' => 0,
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235541'),
'title' => 'Nested 1',
'url' => 'http://nest.ed/1',
'description' => '',
'private' => 0,
- 'tags' => 'tag1 tag2'
+ 'tags' => 'tag1 tag2',
+ 'shorturl' => 'KyDNKA',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/1')
);
$this->assertEquals(
array(
- 'linkdate' => '20160225_205542',
+ 'id' => 1,
+ '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'
+ 'tags' => 'folder1 tag1 tag2',
+ 'shorturl' => 'T2LnXg',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
);
$this->assertEquals(
array(
- 'linkdate' => '20160225_205547',
+ 'id' => 2,
+ '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'
+ 'tags' => 'folder1 tag3 tag4',
+ 'shorturl' => '46SZxA',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
);
$this->assertEquals(
array(
- 'linkdate' => '20160202_172222',
+ 'id' => 3,
+ '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'
+ 'tags' => 'folder2',
+ 'shorturl' => '4UHOSw',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
);
$this->assertEquals(
array(
- 'linkdate' => '20160119_200227',
+ 'id' => 4,
+ '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'
+ 'tags' => 'folder2',
+ 'shorturl' => 'yfzwbw',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
);
$this->assertEquals(
array(
- 'linkdate' => '20160202_172223',
+ 'id' => 5,
+ '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'
+ 'tags' => 'folder3 folder3-1 tag3',
+ 'shorturl' => 'UwxIUQ',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
);
$this->assertEquals(
array(
- 'linkdate' => '20160119_200228',
+ 'id' => 6,
+ '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'
+ 'tags' => 'folder3 folder3-1',
+ 'shorturl' => 'p8dyZg',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
);
$this->assertEquals(
array(
- 'linkdate' => '20160229_081541',
+ 'id' => 7,
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160229_111541'),
'title' => 'Nested 2',
'url' => 'http://nest.ed/2',
'description' => '',
'private' => 0,
- 'tags' => 'tag4'
+ 'tags' => 'tag4',
+ 'shorturl' => 'Gt3Uug',
),
$this->linkDb->getLinkFromUrl('http://nest.ed/2')
);
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
);
+
$this->assertEquals(2, count($this->linkDb));
$this->assertEquals(1, count_private($this->linkDb));
$this->assertEquals(
array(
- 'linkdate' => '20001010_105536',
+ 'id' => 0,
+ // 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'
+ 'tags' => 'private secret',
+ 'shorturl' => 'EokDtA',
),
$this->linkDb->getLinkFromUrl('https://private.tld')
);
$this->assertEquals(
array(
- 'linkdate' => '20160225_205548',
+ 'id' => 1,
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
'title' => 'Public stuff',
'url' => 'http://public.tld',
'description' => '',
'private' => 0,
- 'tags' => 'public hello world'
+ 'tags' => 'public hello world',
+ 'shorturl' => 'Er9ddA',
),
$this->linkDb->getLinkFromUrl('http://public.tld')
);
$this->assertEquals(
array(
- 'linkdate' => '20001010_105536',
+ 'id' => 0,
+ // 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'
+ 'tags' => 'private secret',
+ 'shorturl' => 'EokDtA',
),
$this->linkDb->getLinkFromUrl('https://private.tld')
);
$this->assertEquals(
array(
- 'linkdate' => '20160225_205548',
+ 'id' => 1,
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
'title' => 'Public stuff',
'url' => 'http://public.tld',
'description' => '',
'private' => 0,
- 'tags' => 'public hello world'
+ 'tags' => 'public hello world',
+ 'shorturl' => 'Er9ddA',
),
$this->linkDb->getLinkFromUrl('http://public.tld')
);
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
0,
- $this->linkDb['20001010_105536']['private']
+ $this->linkDb[0]['private']
);
$this->assertEquals(
0,
- $this->linkDb['20160225_205548']['private']
+ $this->linkDb[1]['private']
);
}
$this->assertEquals(2, count_private($this->linkDb));
$this->assertEquals(
1,
- $this->linkDb['20001010_105536']['private']
+ $this->linkDb['0']['private']
);
$this->assertEquals(
1,
- $this->linkDb['20160225_205548']['private']
+ $this->linkDb['1']['private']
);
}
$this->assertEquals(2, count_private($this->linkDb));
$this->assertEquals(
1,
- $this->linkDb['20001010_105536']['private']
+ $this->linkDb[0]['private']
);
$this->assertEquals(
1,
- $this->linkDb['20160225_205548']['private']
+ $this->linkDb[1]['private']
);
-
// re-import as public, enable overwriting
$post = array(
'privacy' => 'public',
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
0,
- $this->linkDb['20001010_105536']['private']
+ $this->linkDb[0]['private']
);
$this->assertEquals(
0,
- $this->linkDb['20160225_205548']['private']
+ $this->linkDb[1]['private']
);
}
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
0,
- $this->linkDb['20001010_105536']['private']
+ $this->linkDb['0']['private']
);
$this->assertEquals(
0,
- $this->linkDb['20160225_205548']['private']
+ $this->linkDb['1']['private']
);
// re-import as private, enable overwriting
$this->assertEquals(2, count_private($this->linkDb));
$this->assertEquals(
1,
- $this->linkDb['20001010_105536']['private']
+ $this->linkDb['0']['private']
);
$this->assertEquals(
1,
- $this->linkDb['20160225_205548']['private']
+ $this->linkDb['1']['private']
);
}
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
'tag1 tag2 tag3 private secret',
- $this->linkDb['20001010_105536']['tags']
+ $this->linkDb['0']['tags']
);
$this->assertEquals(
'tag1 tag2 tag3 public hello world',
- $this->linkDb['20160225_205548']['tags']
+ $this->linkDb['1']['tags']
);
}
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
'tag1& tag2 "tag3" private secret',
- $this->linkDb['20001010_105536']['tags']
+ $this->linkDb['0']['tags']
);
$this->assertEquals(
'tag1& tag2 "tag3" public hello world',
- $this->linkDb['20160225_205548']['tags']
+ $this->linkDb['1']['tags']
);
}
/**
- * Ensure each imported bookmark has a unique linkdate
+ * Ensure each imported bookmark has a unique id
*
* See https://github.com/shaarli/Shaarli/issues/351
*/
$this->assertEquals(3, count($this->linkDb));
$this->assertEquals(0, count_private($this->linkDb));
$this->assertEquals(
- '20160225_205548',
- $this->linkDb['20160225_205548']['linkdate']
+ 0,
+ $this->linkDb[0]['id']
);
$this->assertEquals(
- '20160225_205549',
- $this->linkDb['20160225_205549']['linkdate']
+ 1,
+ $this->linkDb[1]['id']
);
$this->assertEquals(
- '20160225_205550',
- $this->linkDb['20160225_205550']['linkdate']
+ 2,
+ $this->linkDb[2]['id']
);
}
}
$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->assertEquals(escape($redirectorUrl), $this->conf->get('redirector.url'));
unlink($sandbox .'.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));
+ }
}
$conf->set('plugins.ISSO_SERVER', 'value');
$str = 'http://randomstr.com/test';
+ $date = '20161118_100001';
$data = array(
'title' => $str,
'links' => array(
array(
+ 'id' => 12,
'url' => $str,
- 'linkdate' => 'abc',
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date),
)
)
);
// plugin data
$this->assertEquals(1, count($data['plugin_end_zone']));
- $this->assertNotFalse(strpos($data['plugin_end_zone'][0], 'abc'));
+ $this->assertNotFalse(strpos(
+ $data['plugin_end_zone'][0],
+ 'data-isso-id="'. $data['links'][0]['id'] .'"'
+ ));
+ $this->assertNotFalse(strpos(
+ $data['plugin_end_zone'][0],
+ 'data-title="'. $data['links'][0]['id'] .'"'
+ ));
$this->assertNotFalse(strpos($data['plugin_end_zone'][0], 'embed.min.js'));
}
$conf->set('plugins.ISSO_SERVER', 'value');
$str = 'http://randomstr.com/test';
+ $date1 = '20161118_100001';
+ $date2 = '20161118_100002';
$data = array(
'title' => $str,
'links' => array(
array(
+ 'id' => 12,
'url' => $str,
- 'linkdate' => 'abc',
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date1),
),
array(
+ 'id' => 13,
'url' => $str . '2',
- 'linkdate' => 'abc2',
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date2),
),
)
);
$conf->set('plugins.ISSO_SERVER', 'value');
$str = 'http://randomstr.com/test';
+ $date = '20161118_100001';
$data = array(
'title' => $str,
'links' => array(
array(
+ 'id' => 12,
'url' => $str,
- 'linkdate' => 'abc',
+ 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date),
)
),
'search_term' => $str
require_once 'plugins/markdown/markdown.php';
/**
- * Class PlugQrcodeTest
- * Unit test for the QR-Code plugin
+ * Class PluginMarkdownTest
+ * Unit test for the Markdown plugin
*/
class PluginMarkdownTest extends PHPUnit_Framework_TestCase
{
))
);
- $data = hook_markdown_render_linklist($data);
- $this->assertEquals($str, $data['links'][0]['description']);
+ $processed = hook_markdown_render_linklist($data);
+ $this->assertEquals($str, $processed['links'][0]['description']);
+
+ $processed = hook_markdown_render_feed($data);
+ $this->assertEquals($str, $processed['links'][0]['description']);
$data = array(
// Columns data
$this->assertEquals($str, $data['cols'][0][0]['formatedDescription']);
}
+ /**
+ * Test that a close value to nomarkdown is not understand as nomarkdown (previous value `.nomarkdown`).
+ */
+ 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->assertContains('<em>', $data['links'][0]['description']);
+ }
+
+ /**
+ * Test hashtag links processed with markdown.
+ */
+ function testMarkdownHashtagLinks()
+ {
+ $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->assertEquals($html, $data);
+ }
+
/**
* Test hashtag links processed with markdown.
*/
*/
class ReferenceLinkDB
{
- public static $NB_LINKS_TOTAL = 7;
+ public static $NB_LINKS_TOTAL = 8;
private $_links = array();
private $_publicCount = 0;
public function __construct()
{
$this->addLink(
+ 41,
'Link title: @website',
'?WDWyig',
'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
0,
- '20150310_114651',
- 'sTuff'
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'),
+ 'sTuff',
+ null,
+ 'WDWyig'
);
$this->addLink(
+ 42,
+ 'Note: I have a big ID but an old date',
+ '?WDWyig',
+ 'Used to test links reordering.',
+ 0,
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100310_101010'),
+ 'ut'
+ );
+
+ $this->addLink(
+ 8,
'Free as in Freedom 2.0 @website',
'https://static.fsf.org/nosvn/faif-2.0.pdf',
'Richard Stallman and the Free Software Revolution. Read this. #hashtag',
0,
- '20150310_114633',
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'),
'free gnu software stallman -exclude stuff hashtag',
- '20160803_093033'
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033')
);
$this->addLink(
+ 7,
'MediaGoblin',
'http://mediagoblin.org/',
'A free software media publishing platform #hashtagOther',
0,
- '20130614_184135',
- 'gnu media web .hidden hashtag'
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
+ 'gnu media web .hidden hashtag',
+ null,
+ 'IuWvgA'
);
$this->addLink(
+ 6,
'w3c-markup-validator',
'https://dvcs.w3.org/hg/markup-validator/summary',
'Mercurial repository for the W3C Validator #private',
1,
- '20141125_084734',
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20141125_084734'),
'css html w3c web Mercurial'
);
$this->addLink(
+ 4,
'UserFriendly - Web Designer',
'http://ars.userfriendly.org/cartoons/?id=20121206',
'Naming conventions... #private',
0,
- '20121206_142300',
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'),
'dev cartoon web'
);
$this->addLink(
+ 1,
'UserFriendly - Samba',
'http://ars.userfriendly.org/cartoons/?id=20010306',
'Tropical printing',
0,
- '20121206_172539',
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'),
'samba cartoon web'
);
$this->addLink(
+ 0,
'Geek and Poke',
'http://geek-and-poke.com/',
'',
1,
- '20121206_182539',
+ DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'),
'dev cartoon tag1 tag2 tag3 tag4 '
);
}
/**
* Adds a new link
*/
- protected function addLink($title, $url, $description, $private, $date, $tags, $updated = '')
+ protected function addLink($id, $title, $url, $description, $private, $date, $tags, $updated = '', $shorturl = '')
{
$link = array(
+ 'id' => $id,
'title' => $title,
'url' => $url,
'description' => $description,
'private' => $private,
- 'linkdate' => $date,
'tags' => $tags,
+ 'created' => $date,
'updated' => $updated,
+ 'shorturl' => $shorturl ? $shorturl : smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id),
);
- $this->_links[$date] = $link;
+ $this->_links[$id] = $link;
if ($private) {
$this->_privateCount++;
{
return $this->_links;
}
+
+ /**
+ * Setter to override link creation.
+ *
+ * @param array $links List of links.
+ */
+ public function setLinks($links)
+ {
+ $this->_links = $links;
+ }
}
-Allow from none
-Deny from all
+<IfModule version_module>
+ <IfVersion >= 2.4>
+ Require all denied
+ </IfVersion>
+ <IfVersion < 2.4>
+ Allow from none
+ Deny from all
+ </IfVersion>
+</IfModule>
+
+<IfModule !version_module>
+ Require all denied
+</IfModule>
{$link=$value}
<div class="dailyEntry">
<div class="dailyEntryPermalink">
- <a href="?{$link.linkdate|smallHash}">
+ <a href="?{$value.shorturl}">
<img src="../images/squiggle2.png" width="25" height="26" title="permalink" alt="permalink">
</a>
</div>
{if="!$hide_timestamps || isLoggedIn()"}
<div class="dailyEntryLinkdate">
- <a href="?{$link.linkdate|smallHash}">{function="strftime('%c', $link.timestamp)"}</a>
+ <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
</div>
{/if}
{if="$link.tags"}
{elseif="$link.description==''"}onload="document.linkform.lf_description.focus();"
{else}onload="document.linkform.lf_tags.focus();"{/if} >
<div id="pageheader">
- {if="$source !== 'firefoxsocialapi'"}
- {include="page.header"}
- {/if}
- <div id="editlinkform">
- <form method="post" name="linkform">
- <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
- <label for="lf_url"><i>URL</i></label><br><input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input"><br>
+ {if="$source !== 'firefoxsocialapi'"}
+ {include="page.header"}
+ {else}
+ <div id="shaarli_title"><a href="{$titleLink}">{$shaarlititle}</a></div>
+ {/if}
+ <div id="editlinkform">
+ <form method="post" name="linkform">
+ <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
+ {if="isset($link.id)"}
+ <input type="hidden" name="lf_id" value="{$link.id}">
+ {/if}
+ <label for="lf_url"><i>URL</i></label><br><input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input"><br>
<label for="lf_title"><i>Title</i></label><br><input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input"><br>
<label for="lf_description"><i>Description</i></label><br><textarea name="lf_description" id="lf_description" rows="4" cols="25">{$link.description}</textarea><br>
<label for="lf_tags"><i>Tags</i></label><br>
{$value}
{/loop}
- {if="($link_is_new && $default_private_links) || $link.private == true"}
+ {if="($link_is_new && $default_private_links) || $link.private == true"}
<input type="checkbox" checked="checked" name="lf_private" id="lf_private">
<label for="lf_private"><i>Private</i></label><br>
{else}
<input type="checkbox" name="lf_private" id="lf_private">
<label for="lf_private"><i>Private</i></label><br>
{/if}
- <input type="submit" value="Save" name="save_edit" class="bigbutton">
- <input type="submit" value="Cancel" name="cancel_edit" class="bigbutton">
- {if="!$link_is_new"}<input type="submit" value="Delete" name="delete_link" class="bigbutton delete" onClick="return confirmDeleteLink();">{/if}
- <input type="hidden" name="token" value="{$token}">
- {if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if}
- </form>
- </div>
+ <input type="submit" value="Save" name="save_edit" class="bigbutton">
+ <input type="submit" value="Cancel" name="cancel_edit" class="bigbutton">
+ {if="!$link_is_new"}<input type="submit" value="Delete" name="delete_link" class="bigbutton delete" onClick="return confirmDeleteLink();">{/if}
+ <input type="hidden" name="token" value="{$token}">
+ {if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if}
+ </form>
+ </div>
</div>
{if="$source !== 'firefoxsocialapi'"}
{include="page.footer"}
<published>{$value.pub_iso_date}</published>
<updated>{$value.up_iso_date}</updated>
{/if}
- <content type="html" xml:lang="{$language}">
- <![CDATA[{$value.description}]]>
- </content>
+ <content type="html" xml:lang="{$language}"><![CDATA[{$value.description}]]></content>
{loop="$value.taglist"}
<category scheme="{$index_url}?searchtags=" term="{$value|strtolower}" label="{$value}" />
{/loop}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
+<meta name="referrer" content="same-origin">
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
<link href="images/favicon.ico#" rel="shortcut icon" type="image/x-icon" />
{loop="$plugins_includes.css_files"}
<link type="text/css" rel="stylesheet" href="{$value}#"/>
{/loop}
-<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle|htmlspecialchars}"/>
\ No newline at end of file
+<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle|htmlspecialchars}"/>
</form>
{loop="$plugins_header.fields_toolbar"}
<form
- {loop="$value"}
- {if="$key!='inputs'"}
- {$key}="{$value}"
- {/if}
+ {loop="$value.attr"}
+ {$key}="{$value}"
{/loop}>
{loop="$value.inputs"}
<input
{if="isLoggedIn()"}
<div class="linkeditbuttons">
<form method="GET" class="buttoneditform">
- <input type="hidden" name="edit_link" value="{$value.linkdate}">
+ <input type="hidden" name="edit_link" value="{$value.id}">
<input type="image" alt="Edit" src="images/edit_icon.png#" title="Edit" class="button_edit">
</form><br>
- <form method="GET" class="buttoneditform">
+ <form method="POST" class="buttoneditform">
+ <input type="hidden" name="lf_linkdate" value="{$value.id}">
<input type="hidden" name="token" value="{$token}">
- <input type="hidden" name="delete_link" value="{$value.linkdate}">
+ <input type="hidden" name="delete_link">
<input type="image" alt="Delete" src="images/delete_icon.png#" title="Delete"
class="button_delete" onClick="return confirmDeleteLink();">
</form>
{if="!$hide_timestamps || isLoggedIn()"}
{$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'}
<span class="linkdate" title="Permalink">
- <a href="?{$value.linkdate|smallHash}">
+ <a href="?{$value.shorturl}">
<span title="{$updated}">
{function="strftime('%c', $value.timestamp)"}
{if="$value.updated_timestamp"}*{/if}
{loop="$action_plugin"}
<div class="paging_privatelinks">
<a
- {loop="$value"}
- {if="$key!='html'"}
- {$key}="{$value}"
- {/if}
+ {loop="$value.attr"}
+ {$key}="{$value}"
{/loop}>
{$value.html}
</a>
<html>
<head>{include="includes"}</head>
<body
-{if="ban_canLogin()"}
+{if="ban_canLogin($conf)"}
{if="empty($username)"}
onload="document.loginform.login.focus();"
{else}
{include="page.header"}
<div id="headerform">
- {if="!ban_canLogin()"}
+ {if="!ban_canLogin($conf)"}
You have been banned from login after too many failed attempts. Try later.
{else}
<form method="post" name="loginform">
<li><a href="?do=daily">Daily</a></li>
{loop="$plugins_header.buttons_toolbar"}
<li><a
- {loop="$value"}
- {if="$key!='html'"}
- {$key}="{$value}"
- {/if}
+ {loop="$value.attr"}
+ {$key}="{$value}"
{/loop}>
{$value.html}
</a></li>
<tr data-line="{$key}" data-order="{$counter}">
<td class="center"><input type="checkbox" name="{$key}" id="{$key}" checked="checked"></td>
<td class="center">
- <a href="#"
+ <a href="#" class="arrow"
onclick="return orderUp(this.parentNode.parentNode.getAttribute('data-order'));">
▲
</a>
- <a href="#"
+ <a href="#" class="arrow"
onclick="return orderDown(this.parentNode.parentNode.getAttribute('data-order'));">
▼
</a>
Then click "✚Add Note" button anytime to start composing a private Note (text post) to your Shaarli.
</span>
</a><br><br>
+
+ {if="$sslenabled"}
<a class="smallbutton" onclick="activateFirefoxSocial(this)">
<b>✚Add to Firefox social</b>
</a>
<a href="#">
<span>⇐ Click on this button to add Shaarli to the "Share this page" button in Firefox.</span>
</a><br><br>
+ {/if}
{loop="$tools_plugin"}
{$value}
<div class="clear"></div>
<script>
+ {if="$sslenabled"}
function activateFirefoxSocial(node) {
var loc = location.href;
var baseURL = loc.substring(0, loc.lastIndexOf("/"));
var activate = new CustomEvent("ActivateSocialFeature");
node.dispatchEvent(activate);
}
-
+ {/if}
function alertBookmarklet() {
alert('Drag this link to your bookmarks toolbar, or right-click it and choose Bookmark This Link...');
return false;