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
|
composer.lock
|
||||||
/vendor/
|
/vendor/
|
||||||
|
|
||||||
# Ignore test output
|
# Ignore test data & output
|
||||||
|
coverage
|
||||||
|
tests/datastore.php
|
||||||
phpmd.html
|
phpmd.html
|
||||||
|
|
31
Makefile
31
Makefile
|
@ -8,12 +8,15 @@
|
||||||
# - install/update test dependencies:
|
# - install/update test dependencies:
|
||||||
# $ composer install # 1st setup
|
# $ composer install # 1st setup
|
||||||
# $ composer update
|
# $ composer update
|
||||||
|
# - install Xdebug for PHPUnit code coverage reports:
|
||||||
|
# - see http://xdebug.org/docs/install
|
||||||
|
# - enable in php.ini
|
||||||
|
|
||||||
BIN = vendor/bin
|
BIN = vendor/bin
|
||||||
PHP_SOURCE = index.php
|
PHP_SOURCE = index.php application tests
|
||||||
MESS_DETECTOR_RULES = cleancode,codesize,controversial,design,naming,unusedcode
|
PHP_COMMA_SOURCE = index.php,application,tests
|
||||||
|
|
||||||
all: static_analysis_summary
|
all: static_analysis_summary test
|
||||||
|
|
||||||
##
|
##
|
||||||
# Concise status of the project
|
# Concise status of the project
|
||||||
|
@ -21,6 +24,7 @@ all: static_analysis_summary
|
||||||
##
|
##
|
||||||
|
|
||||||
static_analysis_summary: code_sniffer_source copy_paste mess_detector_summary
|
static_analysis_summary: code_sniffer_source copy_paste mess_detector_summary
|
||||||
|
@echo
|
||||||
|
|
||||||
##
|
##
|
||||||
# PHP_CodeSniffer
|
# PHP_CodeSniffer
|
||||||
|
@ -62,6 +66,7 @@ copy_paste:
|
||||||
# Detects PHP syntax errors, sorted by category
|
# Detects PHP syntax errors, sorted by category
|
||||||
# Rules documentation: http://phpmd.org/rules/index.html
|
# Rules documentation: http://phpmd.org/rules/index.html
|
||||||
##
|
##
|
||||||
|
MESS_DETECTOR_RULES = cleancode,codesize,controversial,design,naming,unusedcode
|
||||||
|
|
||||||
mess_title:
|
mess_title:
|
||||||
@echo "-----------------"
|
@echo "-----------------"
|
||||||
|
@ -70,11 +75,11 @@ mess_title:
|
||||||
|
|
||||||
### - all warnings
|
### - all warnings
|
||||||
mess_detector: mess_title
|
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
|
### - all warnings + HTML output contains links to PHPMD's documentation
|
||||||
mess_detector_html:
|
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
|
--reportfile phpmd.html || exit 0
|
||||||
|
|
||||||
### - warnings grouped by message, sorted by descending frequency order
|
### - 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
|
### - summary: number of warnings by rule set
|
||||||
mess_detector_summary: mess_title
|
mess_detector_summary: mess_title
|
||||||
@for rule in $$(echo $(MESS_DETECTOR_RULES) | tr ',' ' '); do \
|
@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"; \
|
printf "$$warnings\t$$rule\n"; \
|
||||||
done;
|
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
|
# Targets for repository and documentation maintenance
|
||||||
##
|
##
|
||||||
|
|
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": {},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpmd/phpmd" : "@stable",
|
"phpmd/phpmd" : "@stable",
|
||||||
|
"phpunit/phpunit": "4.6.*",
|
||||||
"sebastian/phpcpd": "*",
|
"sebastian/phpcpd": "*",
|
||||||
"squizlabs/php_codesniffer": "2.*"
|
"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(E_ALL^E_WARNING); // See all error except warnings.
|
||||||
//error_reporting(-1); // See all errors (for debugging only)
|
//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
|
include "inc/rain.tpl.class.php"; //include Rain TPL
|
||||||
raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory
|
raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory
|
||||||
raintpl::$cache_dir = $GLOBALS['config']['RAINTPL_TMP']; // cache 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)));
|
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.
|
// In a string, converts URLs to clickable links.
|
||||||
// Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
|
// Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
|
||||||
function text2clickable($url)
|
function text2clickable($url)
|
||||||
|
@ -536,20 +525,6 @@ function getMaxFileSize()
|
||||||
return $maxsize;
|
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)
|
/* 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)
|
(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. */
|
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.
|
// Output the last N links in RSS 2.0 format.
|
||||||
function showRSS()
|
function showRSS()
|
||||||
|
@ -941,7 +702,7 @@ function showRSS()
|
||||||
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; }
|
$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:
|
// 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:
|
// Optionally filter the results:
|
||||||
$linksToDisplay=array();
|
$linksToDisplay=array();
|
||||||
|
@ -1019,7 +780,7 @@ function showATOM()
|
||||||
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; }
|
$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:
|
// 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:
|
// Optionally filter the results:
|
||||||
|
@ -1104,7 +865,7 @@ function showDailyRSS()
|
||||||
$cache = new pageCache(pageUrl(),startsWith($query,'do=dailyrss') && !isLoggedIn());
|
$cache = new pageCache(pageUrl(),startsWith($query,'do=dailyrss') && !isLoggedIn());
|
||||||
$cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; }
|
$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:
|
// 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
|
/* Some Shaarlies may have very few links, so we need to look
|
||||||
back in time (rsort()) until we have enough days ($nb_of_days).
|
back in time (rsort()) until we have enough days ($nb_of_days).
|
||||||
|
@ -1172,7 +933,7 @@ function showDailyRSS()
|
||||||
// "Daily" page.
|
// "Daily" page.
|
||||||
function showDaily()
|
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.
|
$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)
|
// Render HTML page (according to URL parameters and user rights)
|
||||||
function renderPage()
|
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.
|
// -------- Display login form.
|
||||||
if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=login'))
|
if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=login'))
|
||||||
|
@ -1822,7 +1583,7 @@ function renderPage()
|
||||||
function importFile()
|
function importFile()
|
||||||
{
|
{
|
||||||
if (!(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'])) { die('Not allowed.'); }
|
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'];
|
$filename=$_FILES['filetoupload']['name'];
|
||||||
$filesize=$_FILES['filetoupload']['size'];
|
$filesize=$_FILES['filetoupload']['size'];
|
||||||
$data=file_get_contents($_FILES['filetoupload']['tmp_name']);
|
$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