LinkDB: move to a proper file, add test coverage
Relates to #71 LinkDB - move to application/LinkDB.php - code cleanup - indentation - whitespaces - formatting - comment cleanup - add missing documentation - unify formatting Test coverage for LinkDB - constructor - public / private access - link-related methods Shaarli utilities (LinkDB dependencies) - move startsWith() and endsWith() functions to application/Utils.php - add test coverage Dev utilities - Composer: add PHPUnit to dev dependencies - Makefile: - update lint targets - add test targets - generate coverage reports Signed-off-by: VirtualTam <virtualtam@flibidi.net>
This commit is contained in:
parent
cbecab7735
commit
ca74886f30
12 changed files with 1231 additions and 257 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -16,5 +16,7 @@ pagecache
|
|||
composer.lock
|
||||
/vendor/
|
||||
|
||||
# Ignore test output
|
||||
# Ignore test data & output
|
||||
coverage
|
||||
tests/datastore.php
|
||||
phpmd.html
|
||||
|
|
33
Makefile
33
Makefile
|
@ -8,12 +8,15 @@
|
|||
# - 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
|
||||
|
@ -21,6 +24,7 @@ all: static_analysis_summary
|
|||
##
|
||||
|
||||
static_analysis_summary: code_sniffer_source copy_paste mess_detector_summary
|
||||
@echo
|
||||
|
||||
##
|
||||
# PHP_CodeSniffer
|
||||
|
@ -62,6 +66,7 @@ copy_paste:
|
|||
# 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 "-----------------"
|
||||
|
@ -70,11 +75,11 @@ mess_title:
|
|||
|
||||
### - 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
|
||||
|
@ -85,10 +90,24 @@ mess_detector_grouped: mess_title
|
|||
### - 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
|
||||
##
|
||||
|
@ -107,4 +126,4 @@ doc: clean
|
|||
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;
|
||||
done;
|
||||
|
|
2
application/.htaccess
Normal file
2
application/.htaccess
Normal file
|
@ -0,0 +1,2 @@
|
|||
Allow from none
|
||||
Deny from all
|
412
application/LinkDB.php
Normal file
412
application/LinkDB.php
Normal file
|
@ -0,0 +1,412 @@
|
|||
<?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()
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
?>
|
45
application/Utils.php
Normal file
45
application/Utils.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?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);
|
||||
}
|
||||
?>
|
|
@ -8,6 +8,7 @@
|
|||
"require": {},
|
||||
"require-dev": {
|
||||
"phpmd/phpmd" : "@stable",
|
||||
"phpunit/phpunit": "4.6.*",
|
||||
"sebastian/phpcpd": "*",
|
||||
"squizlabs/php_codesniffer": "2.*"
|
||||
}
|
||||
|
|
259
index.php
259
index.php
|
@ -68,6 +68,10 @@
|
|||
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
|
||||
|
@ -268,21 +272,6 @@ function nl2br_escaped($html)
|
|||
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)
|
||||
|
@ -536,20 +525,6 @@ function getMaxFileSize()
|
|||
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. */
|
||||
|
@ -710,220 +685,6 @@ public function renderPage($page)
|
|||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
/* 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()
|
||||
|
@ -941,7 +702,7 @@ 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']);
|
||||
|
||||
// Optionally filter the results:
|
||||
$linksToDisplay=array();
|
||||
|
@ -1019,7 +780,7 @@ function showATOM()
|
|||
$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']);
|
||||
|
||||
|
||||
// Optionally filter the results:
|
||||
|
@ -1104,7 +865,7 @@ function showDailyRSS()
|
|||
$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']);
|
||||
|
||||
/* Some Shaarlies may have very few links, so we need to look
|
||||
back in time (rsort()) until we have enough days ($nb_of_days).
|
||||
|
@ -1172,7 +933,7 @@ function showDailyRSS()
|
|||
// "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']);
|
||||
|
||||
|
||||
$day=Date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
|
||||
|
@ -1240,7 +1001,7 @@ function showDaily()
|
|||
// 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']);
|
||||
|
||||
// -------- Display login form.
|
||||
if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=login'))
|
||||
|
@ -1822,7 +1583,7 @@ function renderPage()
|
|||
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']);
|
||||
$filename=$_FILES['filetoupload']['name'];
|
||||
$filesize=$_FILES['filetoupload']['size'];
|
||||
$data=file_get_contents($_FILES['filetoupload']['tmp_name']);
|
||||
|
|
15
phpunit.xml
Normal file
15
phpunit.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?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>
|
2
tests/.htaccess
Normal file
2
tests/.htaccess
Normal file
|
@ -0,0 +1,2 @@
|
|||
Allow from none
|
||||
Deny from all
|
509
tests/LinkDBTest.php
Normal file
509
tests/LinkDBTest.php
Normal file
|
@ -0,0 +1,509 @@
|
|||
<?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'))
|
||||
);
|
||||
}
|
||||
}
|
||||
?>
|
78
tests/UtilsTest.php
Normal file
78
tests/UtilsTest.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?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));
|
||||
}
|
||||
}
|
||||
?>
|
128
tests/utils/ReferenceLinkDB.php
Normal file
128
tests/utils/ReferenceLinkDB.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
?>
|
Loading…
Reference in a new issue