composer.lock
/vendor/
-# Ignore test output
+# Ignore test data & output
+coverage
+tests/datastore.php
phpmd.html
--- /dev/null
+language: php
+php:
+ - 5.6
+ - 5.5
+ - 5.4
+install:
+ - composer self-update
+ - composer install
+script:
+ - make test
# - install/update test dependencies:
# $ composer install # 1st setup
# $ composer update
+# - install Xdebug for PHPUnit code coverage reports:
+# - see http://xdebug.org/docs/install
+# - enable in php.ini
BIN = vendor/bin
-PHP_SOURCE = index.php
-MESS_DETECTOR_RULES = cleancode,codesize,controversial,design,naming,unusedcode
+PHP_SOURCE = index.php application tests
+PHP_COMMA_SOURCE = index.php,application,tests
-all: static_analysis_summary
+all: static_analysis_summary test
##
# Concise status of the project
##
static_analysis_summary: code_sniffer_source copy_paste mess_detector_summary
+ @echo
##
# PHP_CodeSniffer
# Detects PHP syntax errors, sorted by category
# Rules documentation: http://phpmd.org/rules/index.html
##
+MESS_DETECTOR_RULES = cleancode,codesize,controversial,design,naming,unusedcode
mess_title:
@echo "-----------------"
### - all warnings
mess_detector: mess_title
- @$(BIN)/phpmd $(PHP_SOURCE) text $(MESS_DETECTOR_RULES) | sed 's_.*\/__'
+ @$(BIN)/phpmd $(PHP_COMMA_SOURCE) text $(MESS_DETECTOR_RULES) | sed 's_.*\/__'
### - all warnings + HTML output contains links to PHPMD's documentation
mess_detector_html:
- @$(BIN)/phpmd $(PHP_SOURCE) html $(MESS_DETECTOR_RULES) \
+ @$(BIN)/phpmd $(PHP_COMMA_SOURCE) html $(MESS_DETECTOR_RULES) \
--reportfile phpmd.html || exit 0
### - warnings grouped by message, sorted by descending frequency order
### - summary: number of warnings by rule set
mess_detector_summary: mess_title
@for rule in $$(echo $(MESS_DETECTOR_RULES) | tr ',' ' '); do \
- warnings=$$($(BIN)/phpmd $(PHP_SOURCE) text $$rule | wc -l); \
+ warnings=$$($(BIN)/phpmd $(PHP_COMMA_SOURCE) text $$rule | wc -l); \
printf "$$warnings\t$$rule\n"; \
done;
+##
+# PHPUnit
+# Runs unitary and functional tests
+# Generates an HTML coverage report if Xdebug is enabled
+#
+# See phpunit.xml for configuration
+# https://phpunit.de/manual/current/en/appendixes.configuration.html
+##
+test: clean
+ @echo "-------"
+ @echo "PHPUNIT"
+ @echo "-------"
+ @$(BIN)/phpunit tests
+
##
# Targets for repository and documentation maintenance
##
htmldoc:
for file in `find doc/ -maxdepth 1 -name "*.md"`; do \
pandoc -f markdown_github -t html5 -s -c "github-markdown.css" -o doc/`basename $$file .md`.html "$$file"; \
- done;
\ No newline at end of file
+ done;
--- /dev/null
+Allow from none
+Deny from all
--- /dev/null
+<?php
+/**
+ * Data storage for links.
+ *
+ * This object behaves like an associative array.
+ *
+ * Example:
+ * $myLinks = new LinkDB();
+ * echo $myLinks['20110826_161819']['title'];
+ * foreach ($myLinks as $link)
+ * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
+ *
+ * Available keys:
+ * - description: description of the entry
+ * - linkdate: date of the creation of this entry, in the form YYYYMMDD_HHMMSS
+ * (e.g.'20110914_192317')
+ * - private: Is this link private? 0=no, other value=yes
+ * - tags: tags attached to this entry (separated by spaces)
+ * - title Title of the link
+ * - url URL of the link. Can be absolute or relative.
+ * Relative URLs are permalinks (e.g.'?m-ukcw')
+ *
+ * Implements 3 interfaces:
+ * - ArrayAccess: behaves like an associative array;
+ * - Countable: there is a count() method;
+ * - Iterator: usable in foreach () loops.
+ */
+class LinkDB implements Iterator, Countable, ArrayAccess
+{
+ // List of links (associative array)
+ // - key: link date (e.g. "20110823_124546"),
+ // - value: associative array (keys: title, description...)
+ private $links;
+
+ // List of all recorded URLs (key=url, value=linkdate)
+ // for fast reserve search (url-->linkdate)
+ private $urls;
+
+ // List of linkdate keys (for the Iterator interface implementation)
+ private $keys;
+
+ // Position in the $this->keys array (for the Iterator interface)
+ private $position;
+
+ // Is the user logged in? (used to filter private links)
+ private $loggedIn;
+
+ /**
+ * Creates a new LinkDB
+ *
+ * Checks if the datastore exists; else, attempts to create a dummy one.
+ *
+ * @param $isLoggedIn is the user logged in?
+ */
+ function __construct($isLoggedIn)
+ {
+ // FIXME: do not access $GLOBALS, pass the datastore instead
+ $this->loggedIn = $isLoggedIn;
+ $this->checkDB();
+ $this->readdb();
+ }
+
+ /**
+ * Countable - Counts elements of an object
+ */
+ public function count()
+ {
+ return count($this->links);
+ }
+
+ /**
+ * ArrayAccess - Assigns a value to the specified offset
+ */
+ public function offsetSet($offset, $value)
+ {
+ // TODO: use exceptions instead of "die"
+ 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 (empty($offset)) {
+ die('You must specify a key.');
+ }
+ $this->links[$offset] = $value;
+ $this->urls[$value['url']]=$offset;
+ }
+
+ /**
+ * ArrayAccess - Whether or not an offset exists
+ */
+ public function offsetExists($offset)
+ {
+ return array_key_exists($offset, $this->links);
+ }
+
+ /**
+ * ArrayAccess - Unsets an offset
+ */
+ public function offsetUnset($offset)
+ {
+ if (!$this->loggedIn) {
+ // TODO: raise an exception
+ die('You are not authorized to delete a link.');
+ }
+ $url = $this->links[$offset]['url'];
+ unset($this->urls[$url]);
+ unset($this->links[$offset]);
+ }
+
+ /**
+ * ArrayAccess - Returns the value at specified offset
+ */
+ public function offsetGet($offset)
+ {
+ return isset($this->links[$offset]) ? $this->links[$offset] : null;
+ }
+
+ /**
+ * Iterator - Returns the current element
+ */
+ function current()
+ {
+ return $this->links[$this->keys[$this->position]];
+ }
+
+ /**
+ * Iterator - Returns the key of the current element
+ */
+ function key()
+ {
+ return $this->keys[$this->position];
+ }
+
+ /**
+ * Iterator - Moves forward to next element
+ */
+ function next()
+ {
+ ++$this->position;
+ }
+
+ /**
+ * Iterator - Rewinds the Iterator to the first element
+ *
+ * Entries are sorted by date (latest first)
+ */
+ function rewind()
+ {
+ $this->keys = array_keys($this->links);
+ rsort($this->keys);
+ $this->position = 0;
+ }
+
+ /**
+ * Iterator - Checks if current position is valid
+ */
+ function valid()
+ {
+ return isset($this->keys[$this->position]);
+ }
+
+ /**
+ * Checks if the DB directory and file exist
+ *
+ * If no DB file is found, creates a dummy DB.
+ */
+ private function checkDB()
+ {
+ if (file_exists($GLOBALS['config']['DATASTORE'])) {
+ return;
+ }
+
+ // Create a dummy database for example
+ $this->links = array();
+ $link = array(
+ 'title'=>'Shaarli - sebsauvage.net',
+ 'url'=>'http://sebsauvage.net/wiki/doku.php?id=php:shaarli',
+ 'description'=>'Welcome to Shaarli! This is a bookmark. To edit or delete me, you must first login.',
+ 'private'=>0,
+ 'linkdate'=>'20110914_190000',
+ 'tags'=>'opensource software'
+ );
+ $this->links[$link['linkdate']] = $link;
+
+ $link = array(
+ 'title'=>'My secret stuff... - Pastebin.com',
+ 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
+ 'description'=>'SShhhh!! I\'m a private link only YOU can see. You can delete me too.',
+ 'private'=>1,
+ 'linkdate'=>'20110914_074522',
+ 'tags'=>'secretstuff'
+ );
+ $this->links[$link['linkdate']] = $link;
+
+ // Write database to disk
+ // TODO: raise an exception if the file is not write-able
+ file_put_contents(
+ // FIXME: do not use $GLOBALS
+ $GLOBALS['config']['DATASTORE'],
+ PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX
+ );
+ }
+
+ /**
+ * Reads database from disk to memory
+ */
+ private function readdb()
+ {
+
+ // Public links are hidden and user not logged in => nothing to show
+ if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn()) {
+ $this->links = array();
+ return;
+ }
+
+ // Read data
+ // Note that gzinflate is faster than gzuncompress.
+ // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
+ // FIXME: do not use $GLOBALS
+ $this->links = array();
+
+ if (file_exists($GLOBALS['config']['DATASTORE'])) {
+ $this->links = unserialize(gzinflate(base64_decode(
+ substr(file_get_contents($GLOBALS['config']['DATASTORE']),
+ strlen(PHPPREFIX), -strlen(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]);
+ }
+ }
+
+ // Keep the list of the mapping URLs-->linkdate up-to-date.
+ $this->urls = array();
+ foreach ($this->links as $link) {
+ $this->urls[$link['url']] = $link['linkdate'];
+ }
+ }
+
+ /**
+ * Saves the database from memory to disk
+ */
+ public function savedb()
+ {
+ if (!$this->loggedIn) {
+ // TODO: raise an Exception instead
+ die('You are not authorized to change the database.');
+ }
+ file_put_contents(
+ $GLOBALS['config']['DATASTORE'],
+ PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX
+ );
+ invalidateCaches();
+ }
+
+ /**
+ * Returns the link for a given URL, or False if it does not exist.
+ */
+ public function getLinkFromUrl($url)
+ {
+ if (isset($this->urls[$url])) {
+ return $this->links[$this->urls[$url]];
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of links corresponding to a full-text search
+ *
+ * Searches:
+ * - in the URLs, title and description;
+ * - are case-insensitive.
+ *
+ * Example:
+ * print_r($mydb->filterFulltext('hollandais'));
+ *
+ * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
+ * - allows to perform searches on Unicode text
+ * - see https://github.com/shaarli/Shaarli/issues/75 for examples
+ */
+ public function filterFulltext($searchterms)
+ {
+ // FIXME: explode(' ',$searchterms) and perform a AND search.
+ // FIXME: accept double-quotes to search for a string "as is"?
+ $filtered = array();
+ $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8');
+ $keys = ['title', 'description', 'url', 'tags'];
+
+ foreach ($this->links as $link) {
+ $found = false;
+
+ foreach ($keys as $key) {
+ if (strpos(mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'),
+ $search) !== false) {
+ $found = true;
+ }
+ }
+
+ if ($found) {
+ $filtered[$link['linkdate']] = $link;
+ }
+ }
+ krsort($filtered);
+ return $filtered;
+ }
+
+ /**
+ * Returns the list of links associated with a given list of tags
+ *
+ * You can specify one or more tags, separated by space or a comma, e.g.
+ * print_r($mydb->filterTags('linux programming'));
+ */
+ public function filterTags($tags, $casesensitive=false)
+ {
+ // Same as above, we use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
+ // FIXME: is $casesensitive ever true?
+ $t = str_replace(
+ ',', ' ',
+ ($casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'))
+ );
+
+ $searchtags = explode(' ', $t);
+ $filtered = array();
+
+ foreach ($this->links as $l) {
+ $linktags = explode(
+ ' ',
+ ($casesensitive ? $l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8'))
+ );
+
+ if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) {
+ $filtered[$l['linkdate']] = $l;
+ }
+ }
+ krsort($filtered);
+ return $filtered;
+ }
+
+
+ /**
+ * Returns the list of articles for a given day, chronologically sorted
+ *
+ * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
+ * print_r($mydb->filterDay('20120125'));
+ */
+ public function filterDay($day)
+ {
+ // TODO: check input format
+ $filtered = array();
+ foreach ($this->links as $l) {
+ if (startsWith($l['linkdate'], $day)) {
+ $filtered[$l['linkdate']] = $l;
+ }
+ }
+ ksort($filtered);
+ return $filtered;
+ }
+
+ /**
+ * Returns the article corresponding to a smallHash
+ */
+ public function filterSmallHash($smallHash)
+ {
+ $filtered = array();
+ foreach ($this->links as $l) {
+ if ($smallHash == smallHash($l['linkdate'])) {
+ // Yes, this is ugly and slow
+ $filtered[$l['linkdate']] = $l;
+ return $filtered;
+ }
+ }
+ return $filtered;
+ }
+
+ /**
+ * Returns the list of all tags
+ * Output: associative array key=tags, value=0
+ */
+ public function allTags()
+ {
+ $tags = array();
+ foreach ($this->links as $link) {
+ foreach (explode(' ', $link['tags']) as $tag) {
+ if (!empty($tag)) {
+ $tags[$tag] = (empty($tags[$tag]) ? 1 : $tags[$tag] + 1);
+ }
+ }
+ }
+ // Sort tags by usage (most used tag first)
+ arsort($tags);
+ return $tags;
+ }
+
+ /**
+ * Returns the list of days containing articles (oldest first)
+ * Output: An array containing days (in format YYYYMMDD).
+ */
+ public function days()
+ {
+ $linkDays = array();
+ foreach (array_keys($this->links) as $day) {
+ $linkDays[substr($day, 0, 8)] = 0;
+ }
+ $linkDays = array_keys($linkDays);
+ sort($linkDays);
+ return $linkDays;
+ }
+}
+?>
--- /dev/null
+<?php
+/**
+ * Shaarli utilities
+ */
+
+/**
+ * Returns the small hash of a string, using RFC 4648 base64url format
+ *
+ * Small hashes:
+ * - are unique (well, as unique as crc32, at last)
+ * - are always 6 characters long.
+ * - only use the following characters: a-z A-Z 0-9 - _ @
+ * - 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
+ */
+function smallHash($text)
+{
+ $t = rtrim(base64_encode(hash('crc32', $text, true)), '=');
+ return strtr($t, '+/', '-_');
+}
+
+/**
+ * Tells if a string start with a substring
+ */
+function startsWith($haystack, $needle, $case=true)
+{
+ if ($case) {
+ return (strcmp(substr($haystack, 0, strlen($needle)), $needle) === 0);
+ }
+ return (strcasecmp(substr($haystack, 0, strlen($needle)), $needle) === 0);
+}
+
+/**
+ * Tells if a string ends with a substring
+ */
+function endsWith($haystack, $needle, $case=true)
+{
+ if ($case) {
+ return (strcmp(substr($haystack, strlen($haystack) - strlen($needle)), $needle) === 0);
+ }
+ return (strcasecmp(substr($haystack, strlen($haystack) - strlen($needle)), $needle) === 0);
+}
+?>
"require": {},
"require-dev": {
"phpmd/phpmd" : "@stable",
+ "phpunit/phpunit": "4.6.*",
"sebastian/phpcpd": "*",
"squizlabs/php_codesniffer": "2.*"
}
margin-left:24px;
}
+.tagfilter div.awesomplete {
+ width: inherit;
+}
+
.tagfilter #tagfilter_value {
- width: 10%;
+ width: 100%;
+ display: inline;
+}
+
+.tagfilter li {
+ color: black;
}
.tagfilter input.bigbutton, .searchform input.bigbutton, .addform input.bigbutton {
font-size: inherit;
}
+#headerform label {
+ margin-right: 10px;
+}
+
+#headerform label[for=longlastingsession] {
+ display: block;
+ width: 100%;
+ margin-top: 5px;
+}
+
#toolsdiv {
color: #ffffff;
padding: 5px 5px 5px 5px;
margin: 3px;
}
+ #headerform label {
+ width: 100%;
+ display: block;
+ height: auto;
+ line-height: 25px;
+ padding-bottom: 10px;
+ }
+
+ #headerform label input[type=text],
+ #headerform label input[type=password]{
+ float: right;
+ width: 70%;
+ }
+
.searchform, .tagfilter {
display: block !important;
margin: 0px 3px 7px 0px !important;
error_reporting(E_ALL^E_WARNING); // See all error except warnings.
//error_reporting(-1); // See all errors (for debugging only)
+// Shaarli library
+require_once 'application/LinkDB.php';
+require_once 'application/Utils.php';
+
include "inc/rain.tpl.class.php"; //include Rain TPL
raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory
raintpl::$cache_dir = $GLOBALS['config']['RAINTPL_TMP']; // cache directory
return str_replace('>','>',str_replace('<','<',nl2br($html)));
}
-/* Returns the small hash of a string, using RFC 4648 base64url format
- e.g. smallHash('20111006_131924') --> yZH23w
- Small hashes:
- - are unique (well, as unique as crc32, at last)
- - are always 6 characters long.
- - only use the following characters: a-z A-Z 0-9 - _ @
- - are NOT cryptographically secure (they CAN be forged)
- In Shaarli, they are used as a tinyurl-like link to individual entries.
-*/
-function smallHash($text)
-{
- $t = rtrim(base64_encode(hash('crc32',$text,true)),'=');
- return strtr($t, '+/', '-_');
-}
-
// In a string, converts URLs to clickable links.
// Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
function text2clickable($url)
return $maxsize;
}
-// Tells if a string start with a substring or not.
-function startsWith($haystack,$needle,$case=true)
-{
- if($case){return (strcmp(substr($haystack, 0, strlen($needle)),$needle)===0);}
- return (strcasecmp(substr($haystack, 0, strlen($needle)),$needle)===0);
-}
-
-// Tells if a string ends with a substring or not.
-function endsWith($haystack,$needle,$case=true)
-{
- if($case){return (strcmp(substr($haystack, strlen($haystack) - strlen($needle)),$needle)===0);}
- return (strcasecmp(substr($haystack, strlen($haystack) - strlen($needle)),$needle)===0);
-}
-
/* Converts a linkdate time (YYYYMMDD_HHMMSS) of an article to a timestamp (Unix epoch)
(used to build the ADD_DATE attribute in Netscape-bookmarks file)
PS: I could have used strptime(), but it does not exist on Windows. I'm too kind. */
}
}
-// ------------------------------------------------------------------------------------------
-/* Data storage for links.
- This object behaves like an associative array.
- Example:
- $mylinks = new linkdb();
- echo $mylinks['20110826_161819']['title'];
- foreach($mylinks as $link)
- echo $link['title'].' at url '.$link['url'].' ; description:'.$link['description'];
-
- Available keys:
- title : Title of the link
- url : URL of the link. Can be absolute or relative. Relative URLs are permalinks (e.g.'?m-ukcw')
- description : description of the entry
- private : Is this link private? 0=no, other value=yes
- linkdate : date of the creation of this entry, in the form YYYYMMDD_HHMMSS (e.g.'20110914_192317')
- tags : tags attached to this entry (separated by spaces)
-
- We implement 3 interfaces:
- - ArrayAccess so that this object behaves like an associative array.
- - Iterator so that this object can be used in foreach() loops.
- - Countable interface so that we can do a count() on this object.
-*/
-class linkdb implements Iterator, Countable, ArrayAccess
-{
- private $links; // List of links (associative array. Key=linkdate (e.g. "20110823_124546"), value= associative array (keys:title,description...)
- private $urls; // List of all recorded URLs (key=url, value=linkdate) for fast reserve search (url-->linkdate)
- private $keys; // List of linkdate keys (for the Iterator interface implementation)
- private $position; // Position in the $this->keys array. (for the Iterator interface implementation.)
- private $loggedin; // Is the user logged in? (used to filter private links)
-
- // Constructor:
- function __construct($isLoggedIn)
- // Input : $isLoggedIn : is the user logged in?
- {
- $this->loggedin = $isLoggedIn;
- $this->checkdb(); // Make sure data file exists.
- $this->readdb(); // Then read it.
- }
-
- // ---- Countable interface implementation
- public function count() { return count($this->links); }
-
- // ---- ArrayAccess interface implementation
- public function offsetSet($offset, $value)
- {
- 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 (empty($offset)) die('You must specify a key.');
- $this->links[$offset] = $value;
- $this->urls[$value['url']]=$offset;
- }
- public function offsetExists($offset) { return array_key_exists($offset,$this->links); }
- public function offsetUnset($offset)
- {
- if (!$this->loggedin) die('You are not authorized to delete a link.');
- $url = $this->links[$offset]['url']; unset($this->urls[$url]);
- unset($this->links[$offset]);
- }
- public function offsetGet($offset) { return isset($this->links[$offset]) ? $this->links[$offset] : null; }
-
- // ---- Iterator interface implementation
- function rewind() { $this->keys=array_keys($this->links); rsort($this->keys); $this->position=0; } // Start over for iteration, ordered by date (latest first).
- function key() { return $this->keys[$this->position]; } // current key
- function current() { return $this->links[$this->keys[$this->position]]; } // current value
- function next() { ++$this->position; } // go to next item
- function valid() { return isset($this->keys[$this->position]); } // Check if current position is valid.
-
- // ---- Misc methods
- private function checkdb() // Check if db directory and file exists.
- {
- if (!file_exists($GLOBALS['config']['DATASTORE'])) // Create a dummy database for example.
- {
- $this->links = array();
- $link = array('title'=>'Shaarli - sebsauvage.net','url'=>'http://sebsauvage.net/wiki/doku.php?id=php:shaarli','description'=>'Welcome to Shaarli ! This is a bookmark. To edit or delete me, you must first login.','private'=>0,'linkdate'=>'20110914_190000','tags'=>'opensource software');
- $this->links[$link['linkdate']] = $link;
- $link = array('title'=>'My secret stuff... - Pastebin.com','url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=','description'=>'SShhhh!! I\'m a private link only YOU can see. You can delete me too.','private'=>1,'linkdate'=>'20110914_074522','tags'=>'secretstuff');
- $this->links[$link['linkdate']] = $link;
- file_put_contents($GLOBALS['config']['DATASTORE'], PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX); // Write database to disk
- }
- }
-
- // Read database from disk to memory
- private function readdb()
- {
- // Read data
- $this->links=(file_exists($GLOBALS['config']['DATASTORE']) ? unserialize(gzinflate(base64_decode(substr(file_get_contents($GLOBALS['config']['DATASTORE']),strlen(PHPPREFIX),-strlen(PHPSUFFIX))))) : array() );
- // Note that gzinflate is faster than gzuncompress. See: http://www.php.net/manual/en/function.gzdeflate.php#96439
-
- // 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]); }
- }
-
- // Keep the list of the mapping URLs-->linkdate up-to-date.
- $this->urls=array();
- foreach($this->links as $link) { $this->urls[$link['url']]=$link['linkdate']; }
- }
-
- // Save database from memory to disk.
- public function savedb()
- {
- if (!$this->loggedin) die('You are not authorized to change the database.');
- file_put_contents($GLOBALS['config']['DATASTORE'], PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX);
- invalidateCaches();
- }
-
- // Returns the link for a given URL (if it exists). False if it does not exist.
- public function getLinkFromUrl($url)
- {
- if (isset($this->urls[$url])) return $this->links[$this->urls[$url]];
- return false;
- }
-
- // Case insensitive search among links (in the URLs, title and description). Returns filtered list of links.
- // e.g. print_r($mydb->filterFulltext('hollandais'));
- public function filterFulltext($searchterms)
- {
- // FIXME: explode(' ',$searchterms) and perform a AND search.
- // FIXME: accept double-quotes to search for a string "as is"?
- // Using mb_convert_case($val, MB_CASE_LOWER, 'UTF-8') allows us to perform searches on
- // Unicode text. See https://github.com/shaarli/Shaarli/issues/75 for examples.
- $filtered=array();
- $s = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8');
- foreach($this->links as $l)
- {
- $found= (strpos(mb_convert_case($l['title'], MB_CASE_LOWER, 'UTF-8'),$s) !== false)
- || (strpos(mb_convert_case($l['description'], MB_CASE_LOWER, 'UTF-8'),$s) !== false)
- || (strpos(mb_convert_case($l['url'], MB_CASE_LOWER, 'UTF-8'),$s) !== false)
- || (strpos(mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8'),$s) !== false);
- if ($found) $filtered[$l['linkdate']] = $l;
- }
- krsort($filtered);
- return $filtered;
- }
-
- // Filter by tag.
- // You can specify one or more tags (tags can be separated by space or comma).
- // e.g. print_r($mydb->filterTags('linux programming'));
- public function filterTags($tags,$casesensitive=false)
- {
- // Same as above, we use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
- // TODO: is $casesensitive ever true ?
- $t = str_replace(',',' ',($casesensitive?$tags:mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8')));
- $searchtags=explode(' ',$t);
- $filtered=array();
- foreach($this->links as $l)
- {
- $linktags = explode(' ',($casesensitive?$l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8')));
- if (count(array_intersect($linktags,$searchtags)) == count($searchtags))
- $filtered[$l['linkdate']] = $l;
- }
- krsort($filtered);
- return $filtered;
- }
-
- // Filter by day. Day must be in the form 'YYYYMMDD' (e.g. '20120125')
- // Sort order is: older articles first.
- // e.g. print_r($mydb->filterDay('20120125'));
- public function filterDay($day)
- {
- $filtered=array();
- foreach($this->links as $l)
- {
- if (startsWith($l['linkdate'],$day)) $filtered[$l['linkdate']] = $l;
- }
- ksort($filtered);
- return $filtered;
- }
- // Filter by smallHash.
- // Only 1 article is returned.
- public function filterSmallHash($smallHash)
- {
- $filtered=array();
- foreach($this->links as $l)
- {
- if ($smallHash==smallHash($l['linkdate'])) // Yes, this is ugly and slow
- {
- $filtered[$l['linkdate']] = $l;
- return $filtered;
- }
- }
- return $filtered;
- }
-
- // Returns the list of all tags
- // Output: associative array key=tags, value=0
- public function allTags()
- {
- $tags=array();
- foreach($this->links as $link)
- foreach(explode(' ',$link['tags']) as $tag)
- if (!empty($tag)) $tags[$tag]=(empty($tags[$tag]) ? 1 : $tags[$tag]+1);
- arsort($tags); // Sort tags by usage (most used tag first)
- return $tags;
- }
-
- // Returns the list of days containing articles (oldest first)
- // Output: An array containing days (in format YYYYMMDD).
- public function days()
- {
- $linkdays=array();
- foreach(array_keys($this->links) as $day)
- {
- $linkdays[substr($day,0,8)]=0;
- }
- $linkdays=array_keys($linkdays);
- sort($linkdays);
- return $linkdays;
- }
-}
-
// ------------------------------------------------------------------------------------------
// Output the last N links in RSS 2.0 format.
function showRSS()
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; }
// If cached was not found (or not usable), then read the database and build the response:
- $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if user it not logged in).
+ $LINKSDB = new LinkDB(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if user it not logged in).
// Optionally filter the results:
$linksToDisplay=array();
if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
else $linksToDisplay = $LINKSDB;
-
- if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $linksToDisplay = array();
$nblinksToDisplay = 50; // Number of links to display.
if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; }
// If cached was not found (or not usable), then read the database and build the response:
- $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
+ $LINKSDB = new LinkDB(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
// Optionally filter the results:
if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
else if (!empty($_GET['searchtags'])) $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
else $linksToDisplay = $LINKSDB;
-
- if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $linksToDisplay = array();
$nblinksToDisplay = 50; // Number of links to display.
if (!empty($_GET['nb'])) // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
$cache = new pageCache(pageUrl(),startsWith($query,'do=dailyrss') && !isLoggedIn());
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; }
// If cached was not found (or not usable), then read the database and build the response:
- $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
+ $LINKSDB = new LinkDB(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
/* Some Shaarlies may have very few links, so we need to look
back in time (rsort()) until we have enough days ($nb_of_days).
// "Daily" page.
function showDaily()
{
- $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
+ $LINKSDB = new LinkDB(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
$day=Date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
}
$linksToDisplay=$LINKSDB->filterDay($day);
- if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $linksToDisplay = array();
// We pre-format some fields for proper output.
foreach($linksToDisplay as $key=>$link)
{
// Render HTML page (according to URL parameters and user rights)
function renderPage()
{
- $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
+ $LINKSDB = new LinkDB(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
// -------- Display login form.
if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=login'))
if (!empty($_GET['searchterm'])) $links = $LINKSDB->filterFulltext($_GET['searchterm']);
elseif (!empty($_GET['searchtags'])) $links = $LINKSDB->filterTags(trim($_GET['searchtags']));
else $links = $LINKSDB;
-
- if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $links = array();
$body='';
$linksToDisplay=array();
if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=tagcloud'))
{
$tags= $LINKSDB->allTags();
- if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $tags = array();
+
// We sort tags alphabetically, then choose a font size according to count.
// First, find max value.
$maxcount=0; foreach($tags as $key=>$value) $maxcount=max($maxcount,$value);
function importFile()
{
if (!(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'])) { die('Not allowed.'); }
- $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
+ $LINKSDB = new LinkDB(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']); // Read links from database (and filter private links if used it not logged in).
$filename=$_FILES['filetoupload']['name'];
$filesize=$_FILES['filetoupload']['size'];
$data=file_get_contents($_FILES['filetoupload']['tmp_name']);
if (isset($_GET['searchterm'])) // Fulltext search
{
$linksToDisplay = $LINKSDB->filterFulltext(trim($_GET['searchterm']));
- if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $linksToDisplay = array();
$search_crits=htmlspecialchars(trim($_GET['searchterm']));
$search_type='fulltext';
}
elseif (isset($_GET['searchtags'])) // Search by tag
{
$linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
- if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $linksToDisplay = array();
$search_crits=explode(' ',trim($_GET['searchtags']));
$search_type='tags';
}
}
$search_type='permalink';
}
- // We chose to disable all private links and the user isn't logged in, do not return any link.
- else if ($GLOBALS['config']['HIDE_PUBLIC_LINKS'] && !isLoggedIn())
- $linksToDisplay = array();
else
$linksToDisplay = $LINKSDB; // Otherwise, display without filtering.
$PAGE->assign('redirector',empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector']); // Optional redirector URL.
$PAGE->assign('token',$token);
$PAGE->assign('links',$linkDisp);
+ $PAGE->assign('tags', $LINKSDB->allTags());
return;
}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
+ colors="true">
+ <filter>
+ <whitelist addUncoveredFilesFromWhitelist="true">
+ <directory suffix=".php">application</directory>
+ </whitelist>
+ </filter>
+ <logging>
+ <log type="coverage-html" target="coverage" lowUpperBound="30" highLowerBound="80"/>
+ <log type="coverage-text" target="php://stdout" showUncoveredFiles="true"/>
+ </logging>
+</phpunit>
--- /dev/null
+Allow from none
+Deny from all
--- /dev/null
+<?php
+/**
+ * Link datastore tests
+ */
+
+require_once 'application/LinkDB.php';
+require_once 'application/Utils.php';
+require_once 'tests/utils/ReferenceLinkDB.php';
+
+define('PHPPREFIX', '<?php /* ');
+define('PHPSUFFIX', ' */ ?>');
+
+
+/**
+ * Unitary tests for LinkDB
+ */
+class LinkDBTest extends PHPUnit_Framework_TestCase
+{
+ // datastore to test write operations
+ protected static $testDatastore = 'tests/datastore.php';
+ protected static $dummyDatastoreSHA1 = 'e3edea8ea7bb50be4bcb404df53fbb4546a7156e';
+ protected static $refDB = null;
+ protected static $publicLinkDB = null;
+ protected static $privateLinkDB = null;
+
+ /**
+ * Instantiates public and private LinkDBs with test data
+ *
+ * The reference datastore contains public and private links that
+ * will be used to test LinkDB's methods:
+ * - access filtering (public/private),
+ * - link searches:
+ * - by day,
+ * - by tag,
+ * - by text,
+ * - etc.
+ */
+ public static function setUpBeforeClass()
+ {
+ self::$refDB = new ReferenceLinkDB();
+ self::$refDB->write(self::$testDatastore, PHPPREFIX, PHPSUFFIX);
+
+ $GLOBALS['config']['DATASTORE'] = self::$testDatastore;
+ self::$publicLinkDB = new LinkDB(false);
+ self::$privateLinkDB = new LinkDB(true);
+ }
+
+ /**
+ * Resets test data for each test
+ */
+ protected function setUp()
+ {
+ $GLOBALS['config']['DATASTORE'] = self::$testDatastore;
+ if (file_exists(self::$testDatastore)) {
+ unlink(self::$testDatastore);
+ }
+ }
+
+ /**
+ * Allows to test LinkDB's private methods
+ *
+ * @see
+ * https://sebastian-bergmann.de/archives/881-Testing-Your-Privates.html
+ * http://stackoverflow.com/a/2798203
+ */
+ protected static function getMethod($name)
+ {
+ $class = new ReflectionClass('LinkDB');
+ $method = $class->getMethod($name);
+ $method->setAccessible(true);
+ return $method;
+ }
+
+ /**
+ * Instantiate LinkDB objects - logged in user
+ */
+ public function testConstructLoggedIn()
+ {
+ new LinkDB(true);
+ $this->assertFileExists(self::$testDatastore);
+ }
+
+ /**
+ * Instantiate LinkDB objects - logged out or public instance
+ */
+ public function testConstructLoggedOut()
+ {
+ new LinkDB(false);
+ $this->assertFileExists(self::$testDatastore);
+ }
+
+ /**
+ * Attempt to instantiate a LinkDB whereas the datastore is not writable
+ *
+ * @expectedException PHPUnit_Framework_Error_Warning
+ * @expectedExceptionMessageRegExp /failed to open stream: No such file or directory/
+ */
+ public function testConstructDatastoreNotWriteable()
+ {
+ $GLOBALS['config']['DATASTORE'] = 'null/store.db';
+ new LinkDB(false);
+ }
+
+ /**
+ * The DB doesn't exist, ensure it is created with dummy content
+ */
+ public function testCheckDBNew()
+ {
+ $linkDB = new LinkDB(false);
+ unlink(self::$testDatastore);
+ $this->assertFileNotExists(self::$testDatastore);
+
+ $checkDB = self::getMethod('checkDB');
+ $checkDB->invokeArgs($linkDB, array());
+ $this->assertFileExists(self::$testDatastore);
+
+ // ensure the correct data has been written
+ $this->assertEquals(
+ self::$dummyDatastoreSHA1,
+ sha1_file(self::$testDatastore)
+ );
+ }
+
+ /**
+ * The DB exists, don't do anything
+ */
+ public function testCheckDBLoad()
+ {
+ $linkDB = new LinkDB(false);
+ $this->assertEquals(
+ self::$dummyDatastoreSHA1,
+ sha1_file(self::$testDatastore)
+ );
+
+ $checkDB = self::getMethod('checkDB');
+ $checkDB->invokeArgs($linkDB, array());
+
+ // ensure the datastore is left unmodified
+ $this->assertEquals(
+ self::$dummyDatastoreSHA1,
+ sha1_file(self::$testDatastore)
+ );
+ }
+
+ /**
+ * Load an empty DB
+ */
+ public function testReadEmptyDB()
+ {
+ file_put_contents(self::$testDatastore, PHPPREFIX.'S7QysKquBQA='.PHPSUFFIX);
+ $emptyDB = new LinkDB(false);
+ $this->assertEquals(0, sizeof($emptyDB));
+ $this->assertEquals(0, count($emptyDB));
+ }
+
+ /**
+ * Load public links from the DB
+ */
+ public function testReadPublicDB()
+ {
+ $this->assertEquals(
+ self::$refDB->countPublicLinks(),
+ sizeof(self::$publicLinkDB)
+ );
+ }
+
+ /**
+ * Load public and private links from the DB
+ */
+ public function testReadPrivateDB()
+ {
+ $this->assertEquals(
+ self::$refDB->countLinks(),
+ sizeof(self::$privateLinkDB)
+ );
+ }
+
+ /**
+ * Save the links to the DB
+ */
+ public function testSaveDB()
+ {
+ $testDB = new LinkDB(true);
+ $dbSize = sizeof($testDB);
+
+ $link = array(
+ 'title'=>'an additional link',
+ 'url'=>'http://dum.my',
+ 'description'=>'One more',
+ 'private'=>0,
+ 'linkdate'=>'20150518_190000',
+ 'tags'=>'unit test'
+ );
+ $testDB[$link['linkdate']] = $link;
+
+ // TODO: move PageCache to a proper class/file
+ function invalidateCaches() {}
+
+ $testDB->savedb();
+
+ $testDB = new LinkDB(true);
+ $this->assertEquals($dbSize + 1, sizeof($testDB));
+ }
+
+ /**
+ * Count existing links
+ */
+ public function testCount()
+ {
+ $this->assertEquals(
+ self::$refDB->countPublicLinks(),
+ self::$publicLinkDB->count()
+ );
+ $this->assertEquals(
+ self::$refDB->countLinks(),
+ self::$privateLinkDB->count()
+ );
+ }
+
+ /**
+ * List the days for which links have been posted
+ */
+ public function testDays()
+ {
+ $this->assertEquals(
+ ['20121206', '20130614', '20150310'],
+ self::$publicLinkDB->days()
+ );
+
+ $this->assertEquals(
+ ['20121206', '20130614', '20141125', '20150310'],
+ self::$privateLinkDB->days()
+ );
+ }
+
+ /**
+ * The URL corresponds to an existing entry in the DB
+ */
+ public function testGetKnownLinkFromURL()
+ {
+ $link = self::$publicLinkDB->getLinkFromUrl('http://mediagoblin.org/');
+
+ $this->assertNotEquals(false, $link);
+ $this->assertEquals(
+ 'A free software media publishing platform',
+ $link['description']
+ );
+ }
+
+ /**
+ * The URL is not in the DB
+ */
+ public function testGetUnknownLinkFromURL()
+ {
+ $this->assertEquals(
+ false,
+ self::$publicLinkDB->getLinkFromUrl('http://dev.null')
+ );
+ }
+
+ /**
+ * Lists all tags
+ */
+ public function testAllTags()
+ {
+ $this->assertEquals(
+ [
+ 'web' => 3,
+ 'cartoon' => 2,
+ 'gnu' => 2,
+ 'dev' => 1,
+ 'samba' => 1,
+ 'media' => 1,
+ 'software' => 1,
+ 'stallman' => 1,
+ 'free' => 1
+ ],
+ self::$publicLinkDB->allTags()
+ );
+
+ $this->assertEquals(
+ [
+ 'web' => 4,
+ 'cartoon' => 3,
+ 'gnu' => 2,
+ 'dev' => 2,
+ 'samba' => 1,
+ 'media' => 1,
+ 'software' => 1,
+ 'stallman' => 1,
+ 'free' => 1,
+ 'html' => 1,
+ 'w3c' => 1,
+ 'css' => 1,
+ 'Mercurial' => 1
+ ],
+ self::$privateLinkDB->allTags()
+ );
+ }
+
+ /**
+ * Filter links using a tag
+ */
+ public function testFilterOneTag()
+ {
+ $this->assertEquals(
+ 3,
+ sizeof(self::$publicLinkDB->filterTags('web', false))
+ );
+
+ $this->assertEquals(
+ 4,
+ sizeof(self::$privateLinkDB->filterTags('web', false))
+ );
+ }
+
+ /**
+ * Filter links using a tag - case-sensitive
+ */
+ public function testFilterCaseSensitiveTag()
+ {
+ $this->assertEquals(
+ 0,
+ sizeof(self::$privateLinkDB->filterTags('mercurial', true))
+ );
+
+ $this->assertEquals(
+ 1,
+ sizeof(self::$privateLinkDB->filterTags('Mercurial', true))
+ );
+ }
+
+ /**
+ * Filter links using a tag combination
+ */
+ public function testFilterMultipleTags()
+ {
+ $this->assertEquals(
+ 1,
+ sizeof(self::$publicLinkDB->filterTags('dev cartoon', false))
+ );
+
+ $this->assertEquals(
+ 2,
+ sizeof(self::$privateLinkDB->filterTags('dev cartoon', false))
+ );
+ }
+
+ /**
+ * Filter links using a non-existent tag
+ */
+ public function testFilterUnknownTag()
+ {
+ $this->assertEquals(
+ 0,
+ sizeof(self::$publicLinkDB->filterTags('null', false))
+ );
+ }
+
+ /**
+ * Return links for a given day
+ */
+ public function testFilterDay()
+ {
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterDay('20121206'))
+ );
+
+ $this->assertEquals(
+ 3,
+ sizeof(self::$privateLinkDB->filterDay('20121206'))
+ );
+ }
+
+ /**
+ * 404 - day not found
+ */
+ public function testFilterUnknownDay()
+ {
+ $this->assertEquals(
+ 0,
+ sizeof(self::$publicLinkDB->filterDay('19700101'))
+ );
+
+ $this->assertEquals(
+ 0,
+ sizeof(self::$privateLinkDB->filterDay('19700101'))
+ );
+ }
+
+ /**
+ * Use an invalid date format
+ */
+ public function testFilterInvalidDay()
+ {
+ $this->assertEquals(
+ 0,
+ sizeof(self::$privateLinkDB->filterDay('Rainy day, dream away'))
+ );
+
+ // TODO: check input format
+ $this->assertEquals(
+ 6,
+ sizeof(self::$privateLinkDB->filterDay('20'))
+ );
+ }
+
+ /**
+ * Retrieve a link entry with its hash
+ */
+ public function testFilterSmallHash()
+ {
+ $links = self::$privateLinkDB->filterSmallHash('IuWvgA');
+
+ $this->assertEquals(
+ 1,
+ sizeof($links)
+ );
+
+ $this->assertEquals(
+ 'MediaGoblin',
+ $links['20130614_184135']['title']
+ );
+
+ }
+
+ /**
+ * No link for this hash
+ */
+ public function testFilterUnknownSmallHash()
+ {
+ $this->assertEquals(
+ 0,
+ sizeof(self::$privateLinkDB->filterSmallHash('Iblaah'))
+ );
+ }
+
+ /**
+ * Full-text search - result from a link's URL
+ */
+ public function testFilterFullTextURL()
+ {
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterFullText('ars.userfriendly.org'))
+ );
+ }
+
+ /**
+ * Full-text search - result from a link's title only
+ */
+ public function testFilterFullTextTitle()
+ {
+ // use miscellaneous cases
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterFullText('userfriendly -'))
+ );
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterFullText('UserFriendly -'))
+ );
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterFullText('uSeRFrIendlY -'))
+ );
+
+ // use miscellaneous case and offset
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterFullText('RFrIendL'))
+ );
+ }
+
+ /**
+ * Full-text search - result from the link's description only
+ */
+ public function testFilterFullTextDescription()
+ {
+ $this->assertEquals(
+ 1,
+ sizeof(self::$publicLinkDB->filterFullText('media publishing'))
+ );
+ }
+
+ /**
+ * Full-text search - result from the link's tags only
+ */
+ public function testFilterFullTextTags()
+ {
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterFullText('gnu'))
+ );
+ }
+
+ /**
+ * Full-text search - result set from mixed sources
+ */
+ public function testFilterFullTextMixed()
+ {
+ $this->assertEquals(
+ 2,
+ sizeof(self::$publicLinkDB->filterFullText('free software'))
+ );
+ }
+}
+?>
--- /dev/null
+<?php
+/**
+ * Utilities' tests
+ */
+
+require_once 'application/Utils.php';
+
+/**
+ * Unitary tests for Shaarli utilities
+ */
+class UtilsTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Represent a link by its hash
+ */
+ public function testSmallHash()
+ {
+ $this->assertEquals('CyAAJw', smallHash('http://test.io'));
+ $this->assertEquals(6, strlen(smallHash('https://github.com')));
+ }
+
+ /**
+ * Look for a substring at the beginning of a string
+ */
+ public function testStartsWithCaseInsensitive()
+ {
+ $this->assertTrue(startsWith('Lorem ipsum', 'lorem', false));
+ $this->assertTrue(startsWith('Lorem ipsum', 'LoReM i', false));
+ }
+
+ /**
+ * Look for a substring at the beginning of a string (case-sensitive)
+ */
+ public function testStartsWithCaseSensitive()
+ {
+ $this->assertTrue(startsWith('Lorem ipsum', 'Lorem', true));
+ $this->assertFalse(startsWith('Lorem ipsum', 'lorem', true));
+ $this->assertFalse(startsWith('Lorem ipsum', 'LoReM i', true));
+ }
+
+ /**
+ * Look for a substring at the beginning of a string (Unicode)
+ */
+ public function testStartsWithSpecialChars()
+ {
+ $this->assertTrue(startsWith('å!ùµ', 'å!', false));
+ $this->assertTrue(startsWith('µ$åù', 'µ$', true));
+ }
+
+ /**
+ * Look for a substring at the end of a string
+ */
+ public function testEndsWithCaseInsensitive()
+ {
+ $this->assertTrue(endsWith('Lorem ipsum', 'ipsum', false));
+ $this->assertTrue(endsWith('Lorem ipsum', 'm IpsUM', false));
+ }
+
+ /**
+ * Look for a substring at the end of a string (case-sensitive)
+ */
+ public function testEndsWithCaseSensitive()
+ {
+ $this->assertTrue(endsWith('lorem Ipsum', 'Ipsum', true));
+ $this->assertFalse(endsWith('lorem Ipsum', 'ipsum', true));
+ $this->assertFalse(endsWith('lorem Ipsum', 'M IPsuM', true));
+ }
+
+ /**
+ * Look for a substring at the end of a string (Unicode)
+ */
+ public function testEndsWithSpecialChars()
+ {
+ $this->assertTrue(endsWith('å!ùµ', 'ùµ', false));
+ $this->assertTrue(endsWith('µ$åù', 'åù', true));
+ }
+}
+?>
--- /dev/null
+<?php
+/**
+ * Populates a reference datastore to test LinkDB
+ */
+class ReferenceLinkDB
+{
+ private $links = array();
+ private $publicCount = 0;
+ private $privateCount = 0;
+
+ /**
+ * Populates the test DB with reference data
+ */
+ function __construct()
+ {
+ $this->addLink(
+ 'Free as in Freedom 2.0',
+ 'https://static.fsf.org/nosvn/faif-2.0.pdf',
+ 'Richard Stallman and the Free Software Revolution',
+ 0,
+ '20150310_114633',
+ 'free gnu software stallman'
+ );
+
+ $this->addLink(
+ 'MediaGoblin',
+ 'http://mediagoblin.org/',
+ 'A free software media publishing platform',
+ 0,
+ '20130614_184135',
+ 'gnu media web'
+ );
+
+ $this->addLink(
+ 'w3c-markup-validator',
+ 'https://dvcs.w3.org/hg/markup-validator/summary',
+ 'Mercurial repository for the W3C Validator',
+ 1,
+ '20141125_084734',
+ 'css html w3c web Mercurial'
+ );
+
+ $this->addLink(
+ 'UserFriendly - Web Designer',
+ 'http://ars.userfriendly.org/cartoons/?id=20121206',
+ 'Naming conventions...',
+ 0,
+ '20121206_142300',
+ 'dev cartoon web'
+ );
+
+ $this->addLink(
+ 'UserFriendly - Samba',
+ 'http://ars.userfriendly.org/cartoons/?id=20010306',
+ 'Tropical printing',
+ 0,
+ '20121206_172539',
+ 'samba cartoon web'
+ );
+
+ $this->addLink(
+ 'Geek and Poke',
+ 'http://geek-and-poke.com/',
+ '',
+ 1,
+ '20121206_182539',
+ 'dev cartoon'
+ );
+ }
+
+ /**
+ * Adds a new link
+ */
+ protected function addLink($title, $url, $description, $private, $date, $tags)
+ {
+ $link = array(
+ 'title' => $title,
+ 'url' => $url,
+ 'description' => $description,
+ 'private' => $private,
+ 'linkdate' => $date,
+ 'tags' => $tags,
+ );
+ $this->links[$date] = $link;
+
+ if ($private) {
+ $this->privateCount++;
+ return;
+ }
+ $this->publicCount++;
+ }
+
+ /**
+ * Writes data to the datastore
+ */
+ public function write($filename, $prefix, $suffix)
+ {
+ file_put_contents(
+ $filename,
+ $prefix.base64_encode(gzdeflate(serialize($this->links))).$suffix
+ );
+ }
+
+ /**
+ * Returns the number of links in the reference data
+ */
+ public function countLinks()
+ {
+ return $this->publicCount + $this->privateCount;
+ }
+
+ /**
+ * Returns the number of public links in the reference data
+ */
+ public function countPublicLinks()
+ {
+ return $this->publicCount;
+ }
+
+ /**
+ * Returns the number of private links in the reference data
+ */
+ public function countPrivateLinks()
+ {
+ return $this->privateCount;
+ }
+}
+?>
<!DOCTYPE html>
<html>
-<head>{include="includes"}</head>
+<head>
+ <link type="text/css" rel="stylesheet" href="../inc/awesomplete.css" />
+ {include="includes"}
+</head>
<body>
<div id="pageheader">
{include="page.header"}
<div id="headerform" class="search">
<form method="GET" class="searchform" name="searchform"><input type="text" id="searchform_value" name="searchterm" placeholder="Search text" value=""> <input type="submit" value="Search" class="bigbutton"></form>
- <form method="GET" class="tagfilter" name="tagfilter"><input type="text" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" value=""> <input type="submit" value="Search" class="bigbutton"></form>
+ <form method="GET" class="tagfilter" name="tagfilter">
+ <input type="text" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" value="" list="tagsList" autocomplete="off" class="awesomplete" data-minChars="1">
+ <datalist id="tagsList">
+ {loop="$tags"}<option>{$key}</option>{/loop}
+ </datalist>
+ <input type="submit" value="Search" class="bigbutton">
+ </form>
</div>
</div>
return false;
}
</script>
+<script src="inc/awesomplete.min.js#"></script>
</body>
</html>
You have been banned from login after too many failed attempts. Try later.
{else}
<form method="post" name="loginform">
- Login: <input type="text" name="login" tabindex="1">
- Password : <input type="password" name="password" tabindex="2">
- <input type="submit" value="Login" class="bigbutton" tabindex="4"><br>
- <input type="checkbox" name="longlastingsession" id="longlastingsession" tabindex="3"><label for="longlastingsession"> Stay signed in (Do not check on public computers)</label>
+ <label for="login">Login: <input type="text" id="login" name="login" tabindex="1"></label>
+ <label for="password">Password: <input type="password" id="password" name="password" tabindex="2"></label>
+ <input type="submit" value="Login" class="bigbutton" tabindex="4">
+ <label for="longlastingsession">
+ <input type="checkbox" name="longlastingsession" id="longlastingsession" tabindex="3">
+ Stay signed in (Do not check on public computers)</label>
<input type="hidden" name="token" value="{$token}">
{if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl|htmlspecialchars}">{/if}
</form>