]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge pull request #379 from ArthurHoaro/plugin-markdown
authorArthur <arthur@hoa.ro>
Sun, 31 Jan 2016 17:57:29 +0000 (18:57 +0100)
committerArthur <arthur@hoa.ro>
Sun, 31 Jan 2016 17:57:29 +0000 (18:57 +0100)
PLUGIN Markdown

48 files changed:
.gitattributes [new file with mode: 0644]
.travis.yml
Makefile
application/ApplicationUtils.php
application/HttpUtils.php
application/LinkDB.php
application/LinkFilter.php [new file with mode: 0644]
application/LinkUtils.php [new file with mode: 0644]
application/Url.php
application/Utils.php
docker/.htaccess [new file with mode: 0644]
docker/development/Dockerfile [new file with mode: 0644]
docker/development/IMAGE.md [new file with mode: 0644]
docker/development/nginx.conf [new file with mode: 0644]
docker/development/supervised.conf [new file with mode: 0644]
docker/production/Dockerfile [new file with mode: 0644]
docker/production/IMAGE.md [new file with mode: 0644]
docker/production/nginx.conf [new file with mode: 0644]
docker/production/stable/Dockerfile [new file with mode: 0644]
docker/production/stable/IMAGE.md [new file with mode: 0644]
docker/production/stable/nginx.conf [new file with mode: 0644]
docker/production/stable/supervised.conf [new file with mode: 0644]
docker/production/supervised.conf [new file with mode: 0644]
inc/shaarli.css
index.php
plugins/qrcode/qrcode.css [new file with mode: 0644]
plugins/qrcode/qrcode.html
plugins/qrcode/qrcode.php
plugins/qrcode/shaarli-qrcode.js
plugins/wallabag/README.md
plugins/wallabag/WallabagInstance.php [new file with mode: 0644]
plugins/wallabag/config.php.dist
plugins/wallabag/wallabag.html
plugins/wallabag/wallabag.php
shaarli_version.php
tests/HttpUtils/GetHttpUrlTest.php
tests/LinkDBTest.php
tests/LinkFilterTest.php [new file with mode: 0644]
tests/LinkUtilsTest.php [new file with mode: 0644]
tests/Url/UrlTest.php
tests/UtilsTest.php
tests/plugins/PlugQrcodeTest.php
tests/plugins/PluginWallabagTest.php
tests/plugins/WallabagInstanceTest.php [new file with mode: 0644]
tests/utils/ReferenceLinkDB.php
tpl/404.html [new file with mode: 0644]
tpl/linklist.html
tpl/tools.html [changed mode: 0755->0644]

diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..aaf6a39
--- /dev/null
@@ -0,0 +1,31 @@
+# Set default behavior
+*       text=auto eol=lf
+
+# Ensure sources are processed
+*.conf  text
+*.css   text
+*.html  text diff=html
+*.js    text
+*.md    text
+*.php   text diff=php
+Dockerfile      text
+
+# Do not alter images nor minified scripts
+*.ico           binary
+*.jpg           binary
+*.png           binary
+*.min.css       binary
+*.min.js        binary
+
+# Exclude from Git archives
+.gitattributes  export-ignore
+.gitignore      export-ignore
+.travis.yml     export-ignore
+composer.json   export-ignore
+doc/**/*.json   export-ignore
+doc/**/*.md     export-ignore
+docker/         export-ignore
+Doxyfile        export-ignore
+Makefile        export-ignore
+phpunit.xml     export-ignore
+tests/          export-ignore
index a3038c13d8c1e5505f67dab50b96662b751ad42a..7408b2e2dfd918abc4b085d22d1006c647ce1504 100644 (file)
@@ -11,4 +11,5 @@ install:
   - composer install
 script:
   - make clean
+  - make check_permissions
   - make test
index a86f9aa8bbe8bdb0b160db7cea331ba605a3eeb0..75c54f2875d012539e1ff3be563cabacdbf77041 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ BIN = vendor/bin
 PHP_SOURCE = index.php application tests plugins
 PHP_COMMA_SOURCE = index.php,application,tests,plugins
 
-all: static_analysis_summary test
+all: static_analysis_summary check_permissions test
 
 ##
 # Concise status of the project
@@ -98,6 +98,20 @@ mess_detector_summary: mess_title
                printf "$$warnings\t$$rule\n"; \
        done;
 
+##
+# Checks source file & script permissions
+##
+check_permissions:
+       @echo "----------------------"
+       @echo "Check file permissions"
+       @echo "----------------------"
+       @for file in `git ls-files`; do \
+               if [ -x $$file ]; then \
+                       errors=true; \
+                       echo "$${file} is executable"; \
+               fi \
+       done; [ -z $$errors ] || false
+
 ##
 # PHPUnit
 # Runs unitary and functional tests
index 274331e16f6fe21ac444f330b25c69a0d09e1fe5..978fc9da5aa29bb844957ff611da1d8bac5a4003 100644 (file)
@@ -19,7 +19,7 @@ class ApplicationUtils
      */
     public static function getLatestGitVersionCode($url, $timeout=2)
     {
-        list($headers, $data) = get_http_url($url, $timeout);
+        list($headers, $data) = get_http_response($url, $timeout);
 
         if (strpos($headers[0], '200 OK') === false) {
             error_log('Failed to retrieve ' . $url);
index 499220c596c201c098bc2178417c09b7d89f8be1..e2c1cb470f8a9c776358046839b10d3cdbdd3fd5 100644 (file)
@@ -13,7 +13,7 @@
  *  [1] = URL content (downloaded data)
  *
  * Example:
- *  list($headers, $data) = get_http_url('http://sebauvage.net/');
+ *  list($headers, $data) = get_http_response('http://sebauvage.net/');
  *  if (strpos($headers[0], '200 OK') !== false) {
  *      echo 'Data type: '.htmlspecialchars($headers['Content-Type']);
  *  } else {
  * @see http://php.net/manual/en/function.stream-context-create.php
  * @see http://php.net/manual/en/function.get-headers.php
  */
-function get_http_url($url, $timeout = 30, $maxBytes = 4194304)
+function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
 {
+    $urlObj = new Url($url);
+    if (! filter_var($url, FILTER_VALIDATE_URL) || ! $urlObj->isHttp()) {
+        return array(array(0 => 'Invalid HTTP Url'), false);
+    }
+
     $options = array(
         'http' => array(
             'method' => 'GET',
             'timeout' => $timeout,
             'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0)'
-                         .' Gecko/20100101 Firefox/23.0'
+                         .' Gecko/20100101 Firefox/23.0',
+            'request_fulluri' => true,
         )
     );
 
     $context = stream_context_create($options);
+    stream_context_set_default($options);
+
+    list($headers, $finalUrl) = get_redirected_headers($urlObj->cleanup());
+    if (! $headers || strpos($headers[0], '200 OK') === false) {
+        return array($headers, false);
+    }
 
     try {
         // TODO: catch Exception in calling code (thumbnailer)
-        $content = file_get_contents($url, false, $context, -1, $maxBytes);
+        $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes);
     } catch (Exception $exc) {
         return array(array(0 => 'HTTP Error'), $exc->getMessage());
     }
 
-    if (!$content) {
-        return array(array(0 => 'HTTP Error'), '');
+    return array($headers, $content);
+}
+
+/**
+ * Retrieve HTTP headers, following n redirections (temporary and permanent).
+ *
+ * @param string $url initial URL to reach.
+ * @param int $redirectionLimit max redirection follow..
+ *
+ * @return array
+ */
+function get_redirected_headers($url, $redirectionLimit = 3)
+{
+    $headers = get_headers($url, 1);
+
+    // Headers found, redirection found, and limit not reached.
+    if ($redirectionLimit-- > 0
+        && !empty($headers)
+        && (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false)
+        && !empty($headers['Location'])) {
+
+        $redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location'];
+        if ($redirection != $url) {
+            return get_redirected_headers($redirection, $redirectionLimit);
+        }
     }
 
-    return array(get_headers($url, 1), $content);
+    return array($headers, $url);
 }
 
 /**
index f771ac8bf7b1677c99c67ac468ea33009c69a6cc..19ca64355052ba314ab7d9d29587662aa2d94dc7 100644 (file)
  *  - 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.
+ *  - url       URL of the link. Used for displayable links (no redirector, relative, etc.).
+ *              Can be absolute or relative.
  *              Relative URLs are permalinks (e.g.'?m-ukcw')
+ *  - real_url  Absolute processed URL.
  *
  * Implements 3 interfaces:
  *  - ArrayAccess: behaves like an associative array;
@@ -332,114 +334,20 @@ You use the community supported version of the original Shaarli project, by Seba
     }
 
     /**
-     * Returns the list of links corresponding to a full-text search
+     * Filter links.
      *
-     * Searches:
-     *  - in the URLs, title and description;
-     *  - are case-insensitive.
+     * @param string $type          Type of filter.
+     * @param mixed  $request       Search request, string or array.
+     * @param bool   $casesensitive Optional: Perform case sensitive filter
+     * @param bool   $privateonly   Optional: Returns private links only if true.
      *
-     * 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 = array('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'));
+     * @return array filtered links
      */
-    public function filterTags($tags, $casesensitive=false)
+    public function filter($type, $request, $casesensitive = false, $privateonly = 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)
-    {
-        if (! checkDateFormat('Ymd', $day)) {
-            throw new Exception('Invalid date 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;
+        $linkFilter = new LinkFilter($this->_links);
+        $requestFilter = is_array($request) ? implode(' ', $request) : $request;
+        return $linkFilter->filter($type, trim($requestFilter), $casesensitive, $privateonly);
     }
 
     /**
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
new file mode 100644 (file)
index 0000000..cf64737
--- /dev/null
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * Class LinkFilter.
+ *
+ * Perform search and filter operation on link data list.
+ */
+class LinkFilter
+{
+    /**
+     * @var string permalinks.
+     */
+    public static $FILTER_HASH   = 'permalink';
+
+    /**
+     * @var string text search.
+     */
+    public static $FILTER_TEXT   = 'fulltext';
+
+    /**
+     * @var string tag filter.
+     */
+    public static $FILTER_TAG    = 'tags';
+
+    /**
+     * @var string filter by day.
+     */
+    public static $FILTER_DAY    = 'FILTER_DAY';
+
+    /**
+     * @var array all available links.
+     */
+    private $links;
+
+    /**
+     * @param array $links initialization.
+     */
+    public function __construct($links)
+    {
+        $this->links = $links;
+    }
+
+    /**
+     * Filter links according to parameters.
+     *
+     * @param string $type          Type of filter (eg. tags, permalink, etc.).
+     * @param string $request       Filter content.
+     * @param bool   $casesensitive Optional: Perform case sensitive filter if true.
+     * @param bool   $privateonly   Optional: Only returns private links if true.
+     *
+     * @return array filtered link list.
+     */
+    public function filter($type, $request, $casesensitive = false, $privateonly = false)
+    {
+        switch($type) {
+            case self::$FILTER_HASH:
+                return $this->filterSmallHash($request);
+                break;
+            case self::$FILTER_TEXT:
+                return $this->filterFulltext($request, $privateonly);
+                break;
+            case self::$FILTER_TAG:
+                return $this->filterTags($request, $casesensitive, $privateonly);
+                break;
+            case self::$FILTER_DAY:
+                return $this->filterDay($request);
+                break;
+            default:
+                return $this->noFilter($privateonly);
+        }
+    }
+
+    /**
+     * Unknown filter, but handle private only.
+     *
+     * @param bool $privateonly returns private link only if true.
+     *
+     * @return array filtered links.
+     */
+    private function noFilter($privateonly = false)
+    {
+        if (! $privateonly) {
+            krsort($this->links);
+            return $this->links;
+        }
+
+        $out = array();
+        foreach ($this->links as $value) {
+            if ($value['private']) {
+                $out[$value['linkdate']] = $value;
+            }
+        }
+
+        krsort($out);
+        return $out;
+    }
+
+    /**
+     * Returns the shaare corresponding to a smallHash.
+     *
+     * @param string $smallHash permalink hash.
+     *
+     * @return array $filtered array containing permalink data.
+     */
+    private 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 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
+     *
+     * @param string $searchterms search query.
+     * @param bool   $privateonly return only private links if true.
+     *
+     * @return array search results.
+     */
+    private function filterFulltext($searchterms, $privateonly = false)
+    {
+        // 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');
+        $explodedSearch = explode(' ', trim($search));
+        $keys = array('title', 'description', 'url', 'tags');
+
+        // Iterate over every stored link.
+        foreach ($this->links as $link) {
+            $found = false;
+
+            // ignore non private links when 'privatonly' is on.
+            if (! $link['private'] && $privateonly === true) {
+                continue;
+            }
+
+            // Iterate over searchable link fields.
+            foreach ($keys as $key) {
+                // Search full expression.
+                if (strpos(
+                    mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8'),
+                    $search
+                ) !== false) {
+                    $found = true;
+                }
+
+                if ($found) {
+                    break;
+                }
+            }
+
+            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'));
+     *
+     * @param string $tags          list of tags separated by commas or blank spaces.
+     * @param bool   $casesensitive ignore case if false.
+     * @param bool   $privateonly   returns private links only.
+     *
+     * @return array filtered links.
+     */
+    public function filterTags($tags, $casesensitive = false, $privateonly = false)
+    {
+        $searchtags = $this->tagsStrToArray($tags, $casesensitive);
+        $filtered = array();
+
+        foreach ($this->links as $l) {
+            // ignore non private links when 'privatonly' is on.
+            if (! $l['private'] && $privateonly === true) {
+                continue;
+            }
+
+            $linktags = $this->tagsStrToArray($l['tags'], $casesensitive);
+
+            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'));
+     *
+     * @param string $day day to filter.
+     *
+     * @return array all link matching given day.
+     *
+     * @throws Exception if date format is invalid.
+     */
+    public function filterDay($day)
+    {
+        if (! checkDateFormat('Ymd', $day)) {
+            throw new Exception('Invalid date format');
+        }
+
+        $filtered = array();
+        foreach ($this->links as $l) {
+            if (startsWith($l['linkdate'], $day)) {
+                $filtered[$l['linkdate']] = $l;
+            }
+        }
+        ksort($filtered);
+        return $filtered;
+    }
+
+    /**
+     * Convert a list of tags (str) to an array. Also
+     * - handle case sensitivity.
+     * - accepts spaces commas as separator.
+     * - remove private tags for loggedout users.
+     *
+     * @param string $tags          string containing a list of tags.
+     * @param bool   $casesensitive will convert everything to lowercase if false.
+     *
+     * @return array filtered tags string.
+    */
+    public function tagsStrToArray($tags, $casesensitive)
+    {
+        // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
+        $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
+        $tagsOut = str_replace(',', ' ', $tagsOut);
+
+        return explode(' ', trim($tagsOut));
+    }
+}
diff --git a/application/LinkUtils.php b/application/LinkUtils.php
new file mode 100644 (file)
index 0000000..26dd6b6
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * Extract title from an HTML document.
+ *
+ * @param string $html HTML content where to look for a title.
+ *
+ * @return bool|string Extracted title if found, false otherwise.
+ */
+function html_extract_title($html)
+{
+    if (preg_match('!<title>(.*)</title>!is', $html, $matches)) {
+        return trim(str_replace("\n", ' ', $matches[1]));
+    }
+    return false;
+}
+
+/**
+ * Determine charset from downloaded page.
+ * Priority:
+ *   1. HTTP headers (Content type).
+ *   2. HTML content page (tag <meta charset>).
+ *   3. Use a default charset (default: UTF-8).
+ *
+ * @param array  $headers           HTTP headers array.
+ * @param string $htmlContent       HTML content where to look for charset.
+ * @param string $defaultCharset    Default charset to apply if other methods failed.
+ *
+ * @return string Determined charset.
+ */
+function get_charset($headers, $htmlContent, $defaultCharset = 'utf-8')
+{
+    if ($charset = headers_extract_charset($headers)) {
+        return $charset;
+    }
+
+    if ($charset = html_extract_charset($htmlContent)) {
+        return $charset;
+    }
+
+    return $defaultCharset;
+}
+
+/**
+ * Extract charset from HTTP headers if it's defined.
+ *
+ * @param array $headers HTTP headers array.
+ *
+ * @return bool|string Charset string if found (lowercase), false otherwise.
+ */
+function headers_extract_charset($headers)
+{
+    if (! empty($headers['Content-Type']) && strpos($headers['Content-Type'], 'charset=') !== false) {
+        preg_match('/charset="?([^; ]+)/i', $headers['Content-Type'], $match);
+        if (! empty($match[1])) {
+            return strtolower(trim($match[1]));
+        }
+    }
+
+    return false;
+}
+
+/**
+ * Extract charset HTML content (tag <meta charset>).
+ *
+ * @param string $html HTML content where to look for charset.
+ *
+ * @return bool|string Charset string if found, false otherwise.
+ */
+function html_extract_charset($html)
+{
+    // Get encoding specified in HTML header.
+    preg_match('#<meta .*charset="?([^">/]+)"? */?>#Usi', $html, $enc);
+    if (!empty($enc[1])) {
+        return strtolower($enc[1]);
+    }
+
+    return false;
+}
index af43b457961729f3a5c2abc82a407b4a402531ec..a4ac2e73cad2537ab28cd6bb604356bd311d14d7 100644 (file)
@@ -51,6 +51,18 @@ function get_url_scheme($url)
   return $obj_url->getScheme();
 }
 
+/**
+ * Adds a trailing slash at the end of URL if necessary.
+ *
+ * @param string $url URL to check/edit.
+ *
+ * @return string $url URL with a end trailing slash.
+ */
+function add_trailing_slash($url)
+{
+    return $url . (!endsWith($url, '/') ? '/' : '');
+}
+
 /**
  * URL representation and cleanup utilities
  *
@@ -106,7 +118,7 @@ class Url
      */
     public function __construct($url)
     {
-        $this->parts = parse_url($url);
+        $this->parts = parse_url(trim($url));
 
         if (!empty($url) && empty($this->parts['scheme'])) {
             $this->parts['scheme'] = 'http';
@@ -189,4 +201,13 @@ class Url
         }
         return $this->parts['scheme'];
     }
+
+    /**
+     * Test if the Url is an HTTP one.
+     *
+     * @return true is HTTP, false otherwise.
+     */
+    public function isHttp() {
+        return strpos(strtolower($this->parts['scheme']), 'http') !== false;
+    }
 }
index ac8bfbfc07dab77e40261c5b146479ec474ac44d..10d606987c826990e94fad0489f36aad66f6a969 100644 (file)
@@ -3,6 +3,24 @@
  * Shaarli utilities
  */
 
+/**
+ * Logs a message to a text file
+ *
+ * The log format is compatible with fail2ban.
+ *
+ * @param string $logFile  where to write the logs
+ * @param string $clientIp the client's remote IPv4/IPv6 address
+ * @param string $message  the message to log
+ */
+function logm($logFile, $clientIp, $message)
+{
+    file_put_contents(
+        $logFile,
+        date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL,
+        FILE_APPEND
+    );
+}
+
 /**
  *  Returns the small hash of a string, using RFC 4648 base64url format
  *
@@ -64,12 +82,14 @@ function sanitizeLink(&$link)
 
 /**
  * Checks if a string represents a valid date
+
+ * @param string $format The expected DateTime format of the string
+ * @param string $string A string-formatted date
+ *
+ * @return bool whether the string is a valid date
  *
- * @param string        a string-formatted date
- * @param format        the expected DateTime format of the string
- * @return              whether the string is a valid date
- * @see                 http://php.net/manual/en/class.datetime.php
- * @see                 http://php.net/manual/en/datetime.createfromformat.php
+ * @see http://php.net/manual/en/class.datetime.php
+ * @see http://php.net/manual/en/datetime.createfromformat.php
  */
 function checkDateFormat($format, $string)
 {
diff --git a/docker/.htaccess b/docker/.htaccess
new file mode 100644 (file)
index 0000000..b584d98
--- /dev/null
@@ -0,0 +1,2 @@
+Allow from none
+Deny from all
diff --git a/docker/development/Dockerfile b/docker/development/Dockerfile
new file mode 100644 (file)
index 0000000..2ed59b8
--- /dev/null
@@ -0,0 +1,28 @@
+FROM debian:jessie
+MAINTAINER Shaarli Community
+
+RUN apt-get update \
+    && apt-get install -y \
+       nginx-light php5-fpm php5-gd supervisor \
+       git nano
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
+RUN echo "<?php phpinfo(); ?>" > /var/www/index.php
+
+WORKDIR /var/www
+RUN rm -rf html \
+    && git clone https://github.com/shaarli/Shaarli.git shaarli \
+    && chown -R www-data:www-data .
+
+WORKDIR /var/www/shaarli
+RUN composer install
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
diff --git a/docker/development/IMAGE.md b/docker/development/IMAGE.md
new file mode 100644 (file)
index 0000000..e2ff0f0
--- /dev/null
@@ -0,0 +1,10 @@
+## shaarli:dev
+- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
+- [PHP5-FPM](http://php-fpm.org/)
+- [Nginx](http://nginx.org/)
+- [Shaarli](https://github.com/shaarli/Shaarli)
+
+### Development tools
+- [composer](https://getcomposer.org/)
+- [git](http://git-scm.com/)
+- [nano](http://www.nano-editor.org/)
diff --git a/docker/development/nginx.conf b/docker/development/nginx.conf
new file mode 100644 (file)
index 0000000..cda09b5
--- /dev/null
@@ -0,0 +1,64 @@
+user www-data www-data;
+daemon off;
+worker_processes 4;
+
+events {
+    worker_connections  768;
+}
+
+http {
+    include            mime.types;
+    default_type       application/octet-stream;
+    keepalive_timeout  20;
+
+    index index.html index.php;
+
+    server {
+        listen       80;
+        root         /var/www/shaarli;
+
+        access_log  /var/log/nginx/shaarli.access.log;
+        error_log   /var/log/nginx/shaarli.error.log;
+
+        location /phpinfo/ {
+            # add a PHP info page for convenience
+            fastcgi_pass   unix:/var/run/php5-fpm.sock;
+            fastcgi_index  index.php;
+            fastcgi_param  SCRIPT_FILENAME  /var/www/index.php;
+            include fastcgi_params;
+        }
+
+        location ~ /\. {
+            # deny access to dotfiles
+            access_log off;
+            log_not_found off;
+            deny all;
+        }
+        
+        location ~ ~$ {
+            # deny access to temp editor files, e.g. "script.php~"
+            access_log off;
+            log_not_found off;
+            deny all;
+        }
+
+        location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
+            # cache static assets
+            expires    max;
+            add_header Pragma public;
+            add_header Cache-Control "public, must-revalidate, proxy-revalidate";
+        }
+
+        location ~ (index)\.php$ {
+            # filter and proxy PHP requests to PHP-FPM
+            fastcgi_pass   unix:/var/run/php5-fpm.sock;
+            fastcgi_index  index.php;
+            include        fastcgi.conf;
+        }
+
+        location ~ \.php$ {
+            # deny access to all other PHP scripts
+            deny all;
+        }
+    }
+}
diff --git a/docker/development/supervised.conf b/docker/development/supervised.conf
new file mode 100644 (file)
index 0000000..5acd979
--- /dev/null
@@ -0,0 +1,13 @@
+[program:php5-fpm]
+command=/usr/sbin/php5-fpm -F
+priority=5
+autostart=true
+autorestart=true
+
+[program:nginx]
+command=/usr/sbin/nginx
+priority=10
+autostart=true
+autorestart=true
+stdout_events_enabled=true
+stderr_events_enabled=true
diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile
new file mode 100644 (file)
index 0000000..3db4eb5
--- /dev/null
@@ -0,0 +1,20 @@
+FROM debian:jessie
+MAINTAINER Shaarli Community
+
+RUN apt-get update \
+    && apt-get install -y curl nginx-light php5-fpm php5-gd supervisor
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
+
+WORKDIR /var/www
+RUN rm -rf html \
+    && curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xvzf - \
+    && mv Shaarli-master shaarli \
+    && chown -R www-data:www-data shaarli
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
diff --git a/docker/production/IMAGE.md b/docker/production/IMAGE.md
new file mode 100644 (file)
index 0000000..6f827b3
--- /dev/null
@@ -0,0 +1,5 @@
+## shaarli:latest
+- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
+- [PHP5-FPM](http://php-fpm.org/)
+- [Nginx](http://nginx.org/)
+- [Shaarli](https://github.com/shaarli/Shaarli)
diff --git a/docker/production/nginx.conf b/docker/production/nginx.conf
new file mode 100644 (file)
index 0000000..e23c458
--- /dev/null
@@ -0,0 +1,56 @@
+user www-data www-data;
+daemon off;
+worker_processes 4;
+
+events {
+    worker_connections  768;
+}
+
+http {
+    include            mime.types;
+    default_type       application/octet-stream;
+    keepalive_timeout  20;
+
+    index index.html index.php;
+
+    server {
+        listen       80;
+        root         /var/www/shaarli;
+
+        access_log  /var/log/nginx/shaarli.access.log;
+        error_log   /var/log/nginx/shaarli.error.log;
+
+        location ~ /\. {
+            # deny access to dotfiles
+            access_log off;
+            log_not_found off;
+            deny all;
+        }
+        
+        location ~ ~$ {
+            # deny access to temp editor files, e.g. "script.php~"
+            access_log off;
+            log_not_found off;
+            deny all;
+        }
+
+        location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
+            # cache static assets
+            expires    max;
+            add_header Pragma public;
+            add_header Cache-Control "public, must-revalidate, proxy-revalidate";
+        }
+
+        location ~ (index)\.php$ {
+            # filter and proxy PHP requests to PHP-FPM
+            fastcgi_pass   unix:/var/run/php5-fpm.sock;
+            fastcgi_index  index.php;
+            include        fastcgi.conf;
+        }
+
+        location ~ \.php$ {
+            # deny access to all other PHP scripts
+            deny all;
+        }
+    }
+}
diff --git a/docker/production/stable/Dockerfile b/docker/production/stable/Dockerfile
new file mode 100644 (file)
index 0000000..2bb3948
--- /dev/null
@@ -0,0 +1,20 @@
+FROM debian:jessie
+MAINTAINER Shaarli Community
+
+RUN apt-get update \
+    && apt-get install -y curl nginx-light php5-fpm php5-gd supervisor
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
+
+WORKDIR /var/www
+RUN rm -rf html \
+    && curl -L https://github.com/shaarli/Shaarli/archive/stable.tar.gz | tar xvzf - \
+    && mv Shaarli-stable shaarli \
+    && chown -R www-data:www-data shaarli
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
diff --git a/docker/production/stable/IMAGE.md b/docker/production/stable/IMAGE.md
new file mode 100644 (file)
index 0000000..d85b1d7
--- /dev/null
@@ -0,0 +1,5 @@
+## shaarli:stable
+- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
+- [PHP5-FPM](http://php-fpm.org/)
+- [Nginx](http://nginx.org/)
+- [Shaarli (stable)](https://github.com/shaarli/Shaarli/tree/stable)
diff --git a/docker/production/stable/nginx.conf b/docker/production/stable/nginx.conf
new file mode 100644 (file)
index 0000000..e23c458
--- /dev/null
@@ -0,0 +1,56 @@
+user www-data www-data;
+daemon off;
+worker_processes 4;
+
+events {
+    worker_connections  768;
+}
+
+http {
+    include            mime.types;
+    default_type       application/octet-stream;
+    keepalive_timeout  20;
+
+    index index.html index.php;
+
+    server {
+        listen       80;
+        root         /var/www/shaarli;
+
+        access_log  /var/log/nginx/shaarli.access.log;
+        error_log   /var/log/nginx/shaarli.error.log;
+
+        location ~ /\. {
+            # deny access to dotfiles
+            access_log off;
+            log_not_found off;
+            deny all;
+        }
+        
+        location ~ ~$ {
+            # deny access to temp editor files, e.g. "script.php~"
+            access_log off;
+            log_not_found off;
+            deny all;
+        }
+
+        location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
+            # cache static assets
+            expires    max;
+            add_header Pragma public;
+            add_header Cache-Control "public, must-revalidate, proxy-revalidate";
+        }
+
+        location ~ (index)\.php$ {
+            # filter and proxy PHP requests to PHP-FPM
+            fastcgi_pass   unix:/var/run/php5-fpm.sock;
+            fastcgi_index  index.php;
+            include        fastcgi.conf;
+        }
+
+        location ~ \.php$ {
+            # deny access to all other PHP scripts
+            deny all;
+        }
+    }
+}
diff --git a/docker/production/stable/supervised.conf b/docker/production/stable/supervised.conf
new file mode 100644 (file)
index 0000000..5acd979
--- /dev/null
@@ -0,0 +1,13 @@
+[program:php5-fpm]
+command=/usr/sbin/php5-fpm -F
+priority=5
+autostart=true
+autorestart=true
+
+[program:nginx]
+command=/usr/sbin/nginx
+priority=10
+autostart=true
+autorestart=true
+stdout_events_enabled=true
+stderr_events_enabled=true
diff --git a/docker/production/supervised.conf b/docker/production/supervised.conf
new file mode 100644 (file)
index 0000000..5acd979
--- /dev/null
@@ -0,0 +1,13 @@
+[program:php5-fpm]
+command=/usr/sbin/php5-fpm -F
+priority=5
+autostart=true
+autorestart=true
+
+[program:nginx]
+command=/usr/sbin/nginx
+priority=10
+autostart=true
+autorestart=true
+stdout_events_enabled=true
+stderr_events_enabled=true
index 79ba1d693eebbca99348e43b5c820cc35e82ed5f..7a69d575c1f7cbfbe9d005a867082582fd6689ec 100644 (file)
@@ -738,25 +738,6 @@ h1 {
     background: #ffffff;
 }
 
-div#permalinkQrcode {
-    padding: 20px;
-    width: 220px;
-    height: 220px;
-    background-color: #ffffff;
-    border: 1px solid black;
-    position: absolute;
-    top: -100px;
-    left: -100px;
-    text-align: center;
-    font-size: 8pt;
-    z-index: 50;
-    -webkit-box-shadow: 2px 2px 20px 2px #333333;
-    -moz-box-shadow: 2px 2px 20px 2px #333333;
-    -o-box-shadow: 2px 2px 20px 2px #333333;
-    -ms-box-shadow: 2px 2px 20px 2px #333333;
-    box-shadow: 2px 2px 20px 2px #333333;
-}
-
 div.daily {
     font-family: Georgia, 'DejaVu Serif', Norasi, serif;
     background-color: #E6D6BE;
@@ -1119,4 +1100,17 @@ div.dailyNoEntry {
 ul.errors {
     color: red;
     float: left;
-}
\ No newline at end of file
+}
+
+/* 404 page */
+.error-container {
+
+    margin: 50px;
+    margin-top: 20px;
+}
+
+.error-container h1 {
+    text-decoration: none;
+    font-style: normal;
+    color: #80AD48;
+}
index d0876d957721bba0225bc4a7e72cb885698f6662..beba9c32a2541abeb15b74587a7cf1a7ac86783a 100644 (file)
--- a/index.php
+++ b/index.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Shaarli v0.6.1 - Shaare your links...
+ * Shaarli v0.6.2 - Shaare your links...
  *
  * The personal, minimalist, super-fast, no-database Delicious clone.
  *
@@ -119,7 +119,7 @@ $GLOBALS['config']['PUBSUBHUB_URL'] = '';
 /*
  * PHP configuration
  */
-define('shaarli_version', '0.6.1');
+define('shaarli_version', '0.6.2');
 
 // http://server.com/x/shaarli --> /shaarli/
 define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0)));
@@ -151,6 +151,8 @@ require_once 'application/CachedPage.php';
 require_once 'application/FileUtils.php';
 require_once 'application/HttpUtils.php';
 require_once 'application/LinkDB.php';
+require_once 'application/LinkFilter.php';
+require_once 'application/LinkUtils.php';
 require_once 'application/TimeZone.php';
 require_once 'application/Url.php';
 require_once 'application/Utils.php';
@@ -307,14 +309,6 @@ function setup_login_state() {
 $userIsLoggedIn = setup_login_state();
 
 
-// -----------------------------------------------------------------------------------------------
-// Log to text file
-function logm($message)
-{
-    $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n";
-    file_put_contents($GLOBALS['config']['LOG_FILE'], $t, FILE_APPEND);
-}
-
 // ------------------------------------------------------------------------------------------
 // Sniff browser language to display dates in the right format automatically.
 // (Note that is may not work on your server if the corresponding local is not installed.)
@@ -378,10 +372,10 @@ function check_auth($login,$password)
     if ($login==$GLOBALS['login'] && $hash==$GLOBALS['hash'])
     {   // Login/password is correct.
                fillSessionInfo();
-        logm('Login successful');
+        logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Login successful');
         return True;
     }
-    logm('Login failed for user '.$login);
+    logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login);
     return False;
 }
 
@@ -418,7 +412,7 @@ function ban_loginFailed()
     if ($gb['FAILURES'][$ip]>($GLOBALS['config']['BAN_AFTER']-1))
     {
         $gb['BANS'][$ip]=time()+$GLOBALS['config']['BAN_DURATION'];
-        logm('IP address banned from login');
+        logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'IP address banned from login');
     }
     $GLOBALS['IPBANS'] = $gb;
     file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>");
@@ -442,7 +436,7 @@ function ban_canLogin()
         // User is banned. Check if the ban has expired:
         if ($gb['BANS'][$ip]<=time())
         {   // Ban expired, user can try to login again.
-            logm('Ban lifted.');
+            logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Ban lifted.');
             unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]);
             file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>");
             return true; // Ban has expired, user can login.
@@ -478,7 +472,7 @@ if (isset($_POST['login']))
             session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes"
             session_regenerate_id(true);
         }
-        
+
         // Optional redirect after login:
         if (isset($_GET['post'])) {
             $uri = '?post='. urlencode($_GET['post']);
@@ -577,13 +571,6 @@ function linkdate2iso8601($linkdate)
     return date('c',linkdate2timestamp($linkdate)); // 'c' is for ISO 8601 date format.
 }
 
-// Extract title from an HTML document.
-// (Returns an empty string if not found.)
-function html_extract_title($html)
-{
-  return preg_match('!<title>(.*?)</title>!is', $html, $matches) ? trim(str_replace("\n",' ', $matches[1])) : '' ;
-}
-
 // ------------------------------------------------------------------------------------------
 // Token management for XSRF protection
 // Token should be used in any form which acts on data (create,update,delete,import...).
@@ -646,7 +633,7 @@ class pageBuilder
             $this->tpl->assign('versionError', '');
 
         } catch (Exception $exc) {
-            logm($exc->getMessage());
+            logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], $exc->getMessage());
             $this->tpl->assign('newVersion', '');
             $this->tpl->assign('versionError', escape($exc->getMessage()));
         }
@@ -694,6 +681,18 @@ class pageBuilder
         if ($this->tpl===false) $this->initialize(); // Lazy initialization
         $this->tpl->draw($page);
     }
+
+    /**
+    * Render a 404 page (uses the template : tpl/404.tpl)
+    *
+    * usage : $PAGE->render404('The link was deleted')
+    * @param string $message A messate to display what is not found
+    */
+    public function render404($message='The page you are trying to reach does not exist or has been deleted.') {
+        header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
+        $this->tpl->assign('error_message', $message);
+        $this->renderPage('404');
+    }
 }
 
 // ------------------------------------------------------------------------------------------
@@ -730,18 +729,23 @@ function showRSS()
     // Read links from database (and filter private links if user it not logged in).
 
     // Optionally filter the results:
-    $linksToDisplay=array();
-    if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
-    else if (!empty($_GET['searchtags']))   $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
-    else $linksToDisplay = $LINKSDB;
+    if (!empty($_GET['searchterm'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+    }
+    elseif (!empty($_GET['searchtags'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+    }
+    else {
+        $linksToDisplay = $LINKSDB;
+    }
 
     $nblinksToDisplay = 50;  // Number of links to display.
-    if (!empty($_GET['nb']))  // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
-    {
-        $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ;
+    // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
+    if (!empty($_GET['nb'])) {
+        $nblinksToDisplay = $_GET['nb'] == 'all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
     }
 
-    $pageaddr=escape(index_url($_SERVER));
+    $pageaddr = escape(index_url($_SERVER));
     echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">';
     echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>';
     echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n";
@@ -821,15 +825,20 @@ function showATOM()
     );
 
     // Optionally filter the results:
-    $linksToDisplay=array();
-    if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
-    else if (!empty($_GET['searchtags']))   $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
-    else $linksToDisplay = $LINKSDB;
+    if (!empty($_GET['searchterm'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+    }
+    else if (!empty($_GET['searchtags'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+    }
+    else {
+        $linksToDisplay = $LINKSDB;
+    }
 
     $nblinksToDisplay = 50;  // Number of links to display.
-    if (!empty($_GET['nb']))  // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
-    {
-        $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ;
+    // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
+    if (!empty($_GET['nb'])) {
+        $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
     }
 
     $pageaddr=escape(index_url($_SERVER));
@@ -1024,7 +1033,7 @@ function showDaily($pageBuilder)
     }
 
     try {
-        $linksToDisplay = $LINKSDB->filterDay($day);
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_DAY, $day);
     } catch (Exception $exc) {
         error_log($exc);
         $linksToDisplay = array();
@@ -1149,13 +1158,17 @@ function renderPage()
     if ($targetPage == Router::$PAGE_PICWALL)
     {
         // Optionally filter the results:
-        $links=array();
-        if (!empty($_GET['searchterm'])) $links = $LINKSDB->filterFulltext($_GET['searchterm']);
-        elseif (!empty($_GET['searchtags']))   $links = $LINKSDB->filterTags(trim($_GET['searchtags']));
-        else $links = $LINKSDB;
+        if (!empty($_GET['searchterm'])) {
+            $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+        }
+        elseif (! empty($_GET['searchtags'])) {
+            $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+        }
+        else {
+            $links = $LINKSDB;
+        }
 
-        $body='';
-        $linksToDisplay=array();
+        $linksToDisplay = array();
 
         // Get only links which have a thumbnail.
         foreach($links as $link)
@@ -1282,13 +1295,15 @@ function renderPage()
         }
 
         if (isset($params['searchtags'])) {
-            $tags = explode(' ',$params['searchtags']);
-            $tags=array_diff($tags, array($_GET['removetag'])); // Remove value from array $tags.
-            if (count($tags)==0) {
+            $tags = explode(' ', $params['searchtags']);
+            // Remove value from array $tags.
+            $tags = array_diff($tags, array($_GET['removetag']));
+            $params['searchtags'] = implode(' ',$tags);
+
+            if (empty($params['searchtags'])) {
                 unset($params['searchtags']);
-            } else {
-                $params['searchtags'] = implode(' ',$tags);
             }
+
             unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different)
         }
         header('Location: ?'.http_build_query($params));
@@ -1453,21 +1468,23 @@ function renderPage()
     // -------- User wants to rename a tag or delete it
     if ($targetPage == Router::$PAGE_CHANGETAG)
     {
-        if (empty($_POST['fromtag']))
-        {
-            $PAGE->assign('linkcount',count($LINKSDB));
-            $PAGE->assign('token',getToken());
+        if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
+            $PAGE->assign('linkcount', count($LINKSDB));
+            $PAGE->assign('token', getToken());
             $PAGE->assign('tags', $LINKSDB->allTags());
             $PAGE->renderPage('changetag');
             exit;
         }
-        if (!tokenOk($_POST['token'])) die('Wrong token.');
+
+        if (!tokenOk($_POST['token'])) {
+            die('Wrong token.');
+        }
 
         // Delete a tag:
-        if (!empty($_POST['deletetag']) && !empty($_POST['fromtag']))
-        {
+        if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) {
             $needle=trim($_POST['fromtag']);
-            $linksToAlter = $LINKSDB->filterTags($needle,true); // True for case-sensitive tag search.
+            // True for case-sensitive tag search.
+            $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true);
             foreach($linksToAlter as $key=>$value)
             {
                 $tags = explode(' ',trim($value['tags']));
@@ -1481,10 +1498,10 @@ function renderPage()
         }
 
         // Rename a tag:
-        if (!empty($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag']))
-        {
+        if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) {
             $needle=trim($_POST['fromtag']);
-            $linksToAlter = $LINKSDB->filterTags($needle,true); // true for case-sensitive tag search.
+            // True for case-sensitive tag search.
+            $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true);
             foreach($linksToAlter as $key=>$value)
             {
                 $tags = explode(' ',trim($value['tags']));
@@ -1623,7 +1640,7 @@ function renderPage()
 
     // -------- User want to post a new link: Display link edit form.
     if (isset($_GET['post'])) {
-        $url = cleanup_url($_GET['post']);
+        $url = cleanup_url(escape($_GET['post']));
 
         $link_is_new = false;
         // Check if URL is not already in database (in this case, we will edit the existing link)
@@ -1641,35 +1658,24 @@ function renderPage()
             // If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.)
             if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
                 // Short timeout to keep the application responsive
-                list($headers, $data) = get_http_url($url, 4);
-                // FIXME: Decode charset according to specified in either 1) HTTP response headers or 2) <head> in html
+                list($headers, $content) = get_http_response($url, 4);
                 if (strpos($headers[0], '200 OK') !== false) {
-                    // Look for charset in html header.
-                    preg_match('#<meta .*charset=.*>#Usi', $data, $meta);
-
-                    // If found, extract encoding.
-                    if (!empty($meta[0])) {
-                        // Get encoding specified in header.
-                        preg_match('#charset="?(.*)"#si', $meta[0], $enc);
-                        // If charset not found, use utf-8.
-                        $html_charset = (!empty($enc[1])) ? strtolower($enc[1]) : 'utf-8';
-                    }
-                    else {
-                        $html_charset = 'utf-8';
-                    }
-
-                    // Extract title
-                    $title = html_extract_title($data);
-                    if (!empty($title)) {
-                        // Re-encode title in utf-8 if necessary.
-                        $title = ($html_charset == 'iso-8859-1') ? utf8_encode($title) : $title;
+                    // Retrieve charset.
+                    $charset = get_charset($headers, $content);
+                    // Extract title.
+                    $title = html_extract_title($content);
+                    // Re-encode title in utf-8 if necessary.
+                    if (! empty($title) && $charset != 'utf-8') {
+                        $title = mb_convert_encoding($title, $charset, 'utf-8');
                     }
                 }
             }
+
             if ($url == '') {
                 $url = '?' . smallHash($linkdate);
                 $title = 'Note: ';
             }
+
             $link = array(
                 'linkdate' => $linkdate,
                 'title' => $title,
@@ -1865,81 +1871,75 @@ function importFile()
 function buildLinkList($PAGE,$LINKSDB)
 {
     // ---- Filter link database according to parameters
-    $linksToDisplay=array();
-    $search_type='';
-    $search_crits='';
-    if (isset($_GET['searchterm'])) // Fulltext search
-    {
-        $linksToDisplay = $LINKSDB->filterFulltext(trim($_GET['searchterm']));
-        $search_crits=escape(trim($_GET['searchterm']));
-        $search_type='fulltext';
-    }
-    elseif (isset($_GET['searchtags'])) // Search by tag
-    {
-        $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
-        $search_crits=explode(' ',escape(trim($_GET['searchtags'])));
-        $search_type='tags';
-    }
-    elseif (isset($_SERVER['QUERY_STRING']) && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/',$_SERVER['QUERY_STRING'])) // Detect smallHashes in URL
-    {
-        $linksToDisplay = $LINKSDB->filterSmallHash(substr(trim($_SERVER["QUERY_STRING"], '/'),0,6));
-        if (count($linksToDisplay)==0)
-        {
-            header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
-            echo '<h1>404 Not found.</h1>Oh crap. The link you are trying to reach does not exist or has been deleted.';
-            echo '<br>Would you mind <a href="?">clicking here</a>?';
+    $search_type = '';
+    $search_crits = '';
+    $privateonly = !empty($_SESSION['privateonly']) ? true : false;
+
+    // Fulltext search
+    if (isset($_GET['searchterm'])) {
+        $search_crits = escape(trim($_GET['searchterm']));
+        $search_type = LinkFilter::$FILTER_TEXT;
+        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
+    }
+    // Search by tag
+    elseif (isset($_GET['searchtags'])) {
+        $search_crits = explode(' ', escape(trim($_GET['searchtags'])));
+        $search_type = LinkFilter::$FILTER_TAG;
+        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
+    }
+    // Detect smallHashes in URL.
+    elseif (isset($_SERVER['QUERY_STRING'])
+        && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) {
+        $search_type = LinkFilter::$FILTER_HASH;
+        $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6);
+        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits);
+
+        if (count($linksToDisplay) == 0) {
+            $PAGE->render404('The link you are trying to reach does not exist or has been deleted.');
             exit;
         }
-        $search_type='permalink';
     }
-    else
-        $linksToDisplay = $LINKSDB;  // Otherwise, display without filtering.
-
-
-    // Option: Show only private links
-    if (!empty($_SESSION['privateonly']))
-    {
-        $tmp = array();
-        foreach($linksToDisplay as $linkdate=>$link)
-        {
-            if ($link['private']!=0) $tmp[$linkdate]=$link;
-        }
-        $linksToDisplay=$tmp;
+    // Otherwise, display without filtering.
+    else {
+        $linksToDisplay = $LINKSDB->filter('', '', false, $privateonly);
     }
 
     // ---- Handle paging.
-    /* Can someone explain to me why you get the following error when using array_keys() on an object which implements the interface ArrayAccess???
-       "Warning: array_keys() expects parameter 1 to be array, object given in ... "
-       If my class implements ArrayAccess, why won't array_keys() accept it ?  ( $keys=array_keys($linksToDisplay); )
-    */
-    $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // Stupid and ugly. Thanks PHP.
+    $keys = array();
+    foreach ($linksToDisplay as $key => $value) {
+        $keys[] = $key;
+    }
 
     // If there is only a single link, we change on-the-fly the title of the page.
-    if (count($linksToDisplay)==1) $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title'];
+    if (count($linksToDisplay) == 1) {
+        $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title'];
+    }
 
     // Select articles according to paging.
-    $pagecount = ceil(count($keys)/$_SESSION['LINKS_PER_PAGE']);
-    $pagecount = ($pagecount==0 ? 1 : $pagecount);
-    $page=( empty($_GET['page']) ? 1 : intval($_GET['page']));
-    $page = ( $page<1 ? 1 : $page );
-    $page = ( $page>$pagecount ? $pagecount : $page );
-    $i = ($page-1)*$_SESSION['LINKS_PER_PAGE']; // Start index.
-    $end = $i+$_SESSION['LINKS_PER_PAGE'];
-    $linkDisp=array(); // Links to display
+    $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
+    $pagecount = $pagecount == 0 ? 1 : $pagecount;
+    $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
+    $page = $page < 1 ? 1 : $page;
+    $page = $page > $pagecount ? $pagecount : $page;
+    // Start index.
+    $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
+    $end = $i + $_SESSION['LINKS_PER_PAGE'];
+    $linkDisp = array();
     while ($i<$end && $i<count($keys))
     {
         $link = $linksToDisplay[$keys[$i]];
         $link['description'] = format_description($link['description'], $GLOBALS['redirector']);
-        $classLi =  $i%2!=0 ? '' : 'publicLinkHightLight';
-        $link['class'] = ($link['private']==0 ? $classLi : 'private');
-        $link['timestamp']=linkdate2timestamp($link['linkdate']);
-        $taglist = explode(' ',$link['tags']);
+        $classLi =  ($i % 2) != 0 ? '' : 'publicLinkHightLight';
+        $link['class'] = $link['private'] == 0 ? $classLi : 'private';
+        $link['timestamp'] = linkdate2timestamp($link['linkdate']);
+        $taglist = explode(' ', $link['tags']);
         uasort($taglist, 'strcasecmp');
-        $link['taglist']=$taglist;
+        $link['taglist'] = $taglist;
         $link['shorturl'] = smallHash($link['linkdate']);
-        if ($link["url"][0] === '?' && // Check for both signs of a note: starting with ? and 7 chars long. I doubt that you'll post any links that look like this.
-            strlen($link["url"]) === 7) {
-            $link["url"] = index_url($_SERVER) . $link["url"];
+        // Check for both signs of a note: starting with ? and 7 chars long.
+        if ($link['url'][0] === '?' &&
+            strlen($link['url']) === 7) {
+            $link['url'] = index_url($_SERVER) . $link['url'];
         }
 
         $linkDisp[$keys[$i]] = $link;
@@ -1947,13 +1947,21 @@ function buildLinkList($PAGE,$LINKSDB)
     }
 
     // Compute paging navigation
-    $searchterm= ( empty($_GET['searchterm']) ? '' : '&searchterm='.$_GET['searchterm'] );
-    $searchtags= ( empty($_GET['searchtags']) ? '' : '&searchtags='.$_GET['searchtags'] );
-    $paging='';
-    $previous_page_url=''; if ($i!=count($keys)) $previous_page_url='?page='.($page+1).$searchterm.$searchtags;
-    $next_page_url='';if ($page>1) $next_page_url='?page='.($page-1).$searchterm.$searchtags;
+    $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm'];
+    $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags'];
+    $previous_page_url = '';
+    if ($i != count($keys)) {
+        $previous_page_url = '?page=' . ($page+1) . $searchterm . $searchtags;
+    }
+    $next_page_url='';
+    if ($page>1) {
+        $next_page_url = '?page=' . ($page-1) . $searchterm . $searchtags;
+    }
 
-    $token = ''; if (isLoggedIn()) $token=getToken();
+    $token = '';
+    if (isLoggedIn()) {
+        $token = getToken();
+    }
 
     // Fill all template fields.
     $data = array(
@@ -2290,11 +2298,11 @@ function genThumbnail()
         else // This is a flickr page (html)
         {
             // Get the flickr html page.
-            list($headers, $data) = get_http_url($url, 20);
+            list($headers, $content) = get_http_response($url, 20);
             if (strpos($headers[0], '200 OK') !== false)
             {
                 // flickr now nicely provides the URL of the thumbnail in each flickr page.
-                preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!',$data,$matches);
+                preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!', $content, $matches);
                 if (!empty($matches[1])) $imageurl=$matches[1];
 
                 // In albums (and some other pages), the link rel="image_src" is not provided,
@@ -2302,7 +2310,7 @@ function genThumbnail()
                 // <meta property="og:image" content="http://farm4.staticflickr.com/3398/3239339068_25d13535ff_z.jpg" />
                 if ($imageurl=='')
                 {
-                    preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!',$data,$matches);
+                    preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!', $content, $matches);
                     if (!empty($matches[1])) $imageurl=$matches[1];
                 }
             }
@@ -2311,11 +2319,12 @@ function genThumbnail()
         if ($imageurl!='')
         {   // Let's download the image.
             // Image is 240x120, so 10 seconds to download should be enough.
-            list($headers, $data) = get_http_url($imageurl, 10);
+            list($headers, $content) = get_http_response($imageurl, 10);
             if (strpos($headers[0], '200 OK') !== false) {
-                file_put_contents($GLOBALS['config']['CACHEDIR'].'/'.$thumbname,$data); // Save image to cache.
+                // Save image to cache.
+                file_put_contents($GLOBALS['config']['CACHEDIR'].'/' . $thumbname, $content);
                 header('Content-Type: image/jpeg');
-                echo $data;
+                echo $content;
                 return;
             }
         }
@@ -2326,16 +2335,17 @@ function genThumbnail()
         // This is more complex: we have to perform a HTTP request, then parse the result.
         // Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098
         $vid = substr(parse_url($url,PHP_URL_PATH),1);
-        list($headers, $data) = get_http_url('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5);
+        list($headers, $content) = get_http_response('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5);
         if (strpos($headers[0], '200 OK') !== false) {
-            $t = unserialize($data);
+            $t = unserialize($content);
             $imageurl = $t[0]['thumbnail_medium'];
             // Then we download the image and serve it to our client.
-            list($headers, $data) = get_http_url($imageurl, 10);
+            list($headers, $content) = get_http_response($imageurl, 10);
             if (strpos($headers[0], '200 OK') !== false) {
-                file_put_contents($GLOBALS['config']['CACHEDIR'].'/'.$thumbname,$data); // Save image to cache.
+                // Save image to cache.
+                file_put_contents($GLOBALS['config']['CACHEDIR'] . '/' . $thumbname, $content);
                 header('Content-Type: image/jpeg');
-                echo $data;
+                echo $content;
                 return;
             }
         }
@@ -2346,18 +2356,18 @@ function genThumbnail()
         // The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page
         // http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html
         // <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" />
-        list($headers, $data) = get_http_url($url, 5);
+        list($headers, $content) = get_http_response($url, 5);
         if (strpos($headers[0], '200 OK') !== false) {
             // Extract the link to the thumbnail
-            preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!',$data,$matches);
+            preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!', $content, $matches);
             if (!empty($matches[1]))
             {   // Let's download the image.
                 $imageurl=$matches[1];
                 // No control on image size, so wait long enough
-                list($headers, $data) = get_http_url($imageurl, 20);
+                list($headers, $content) = get_http_response($imageurl, 20);
                 if (strpos($headers[0], '200 OK') !== false) {
                     $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname;
-                    file_put_contents($filepath,$data); // Save image to cache.
+                    file_put_contents($filepath, $content); // Save image to cache.
                     if (resizeImage($filepath))
                     {
                         header('Content-Type: image/jpeg');
@@ -2374,18 +2384,19 @@ function genThumbnail()
         // There is no thumbnail available for xkcd comics, so download the whole image and resize it.
         // http://xkcd.com/327/
         // <img src="http://imgs.xkcd.com/comics/exploits_of_a_mom.png" title="<BLABLA>" alt="<BLABLA>" />
-        list($headers, $data) = get_http_url($url, 5);
+        list($headers, $content) = get_http_response($url, 5);
         if (strpos($headers[0], '200 OK') !== false) {
             // Extract the link to the thumbnail
-            preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!',$data,$matches);
+            preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!', $content, $matches);
             if (!empty($matches[1]))
             {   // Let's download the image.
                 $imageurl=$matches[1];
                 // No control on image size, so wait long enough
-                list($headers, $data) = get_http_url($imageurl, 20);
+                list($headers, $content) = get_http_response($imageurl, 20);
                 if (strpos($headers[0], '200 OK') !== false) {
                     $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname;
-                    file_put_contents($filepath,$data); // Save image to cache.
+                    // Save image to cache.
+                    file_put_contents($filepath, $content);
                     if (resizeImage($filepath))
                     {
                         header('Content-Type: image/jpeg');
@@ -2401,10 +2412,11 @@ function genThumbnail()
     {
         // For all other domains, we try to download the image and make a thumbnail.
         // We allow 30 seconds max to download (and downloads are limited to 4 Mb)
-        list($headers, $data) = get_http_url($url, 30);
+        list($headers, $content) = get_http_response($url, 30);
         if (strpos($headers[0], '200 OK') !== false) {
             $filepath=$GLOBALS['config']['CACHEDIR'].'/'.$thumbname;
-            file_put_contents($filepath,$data); // Save image to cache.
+            // Save image to cache.
+            file_put_contents($filepath, $content);
             if (resizeImage($filepath))
             {
                 header('Content-Type: image/jpeg');
diff --git a/plugins/qrcode/qrcode.css b/plugins/qrcode/qrcode.css
new file mode 100644 (file)
index 0000000..0d514a0
--- /dev/null
@@ -0,0 +1,23 @@
+.linkqrcode {
+    display: inline;
+    position: relative;
+}
+
+#permalinkQrcode {
+    position: absolute;
+    z-index: 200;
+    padding: 20px;
+    width: 220px;
+    height: 220px;
+    background-color: #ffffff;
+    border: 1px solid black;
+    top: -110px;
+    left: -110px;
+    text-align: center;
+    font-size: 8pt;
+    box-shadow: 2px 2px 20px 2px #333333;
+}
+
+#permalinkQrcode img {
+    margin-bottom: 5px;
+}
index 58ac50074a1aa4b34cd3bd0515feb589be625313..ffdaf3b82ed00e644add10d3eaf8eb9384e20d69 100644 (file)
@@ -1,3 +1,5 @@
-<a href="http://qrfree.kaywa.com/?l=1&amp;s=8&amp;d=%s" onclick="showQrCode(this); return false;" class="qrcode" data-permalink="%s">
-    <img src="%s/qrcode/qrcode.png" width="13" height="13" title="QR-Code">
-</a>
+<div class="linkqrcode">
+    <a href="http://qrfree.kaywa.com/?l=1&amp;s=8&amp;d=%s" onclick="showQrCode(this); return false;" class="qrcode" data-permalink="%s">
+        <img src="%s/qrcode/qrcode.png" width="13" height="13" title="QR-Code">
+    </a>
+</div>
index 5f6e76a2f8c42d035f0c7026158b463935d953b0..8bc610d1fad120ec34694c4d3c1a3c7501f14e64 100644 (file)
@@ -17,7 +17,11 @@ function hook_qrcode_render_linklist($data)
     $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html');
 
     foreach ($data['links'] as &$value) {
-        $qrcode = sprintf($qrcode_html, $value['real_url'], $value['real_url'], PluginManager::$PLUGINS_PATH);
+        $qrcode = sprintf($qrcode_html,
+            urlencode($value['url']),
+            $value['url'],
+            PluginManager::$PLUGINS_PATH
+        );
         $value['link_plugin'][] = $qrcode;
     }
 
@@ -39,3 +43,19 @@ function hook_qrcode_render_footer($data)
 
     return $data;
 }
+
+/**
+ * When linklist is displayed, include qrcode CSS file.
+ *
+ * @param array $data - header data.
+ *
+ * @return mixed - header data with qrcode CSS file added.
+ */
+function hook_qrcode_render_includes($data)
+{
+    if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
+        $data['css_files'][] = PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.css';
+    }
+
+    return $data;
+}
index 0a8de21de7f7a6c8b3b258e97041ae7e41a5cc60..615f54c72c3cfe42e3a8df6b8d3aa3bfc9a46c1b 100644 (file)
@@ -19,7 +19,7 @@ function showQrCode(caller,loading)
     
     // Build the div which contains the QR-Code:
     var element = document.createElement('div');
-    element.id="permalinkQrcode";
+    element.id = 'permalinkQrcode';
 
        // Make QR-Code div commit sepuku when clicked:
     if ( element.attachEvent ){
@@ -37,6 +37,12 @@ function showQrCode(caller,loading)
         element.appendChild(image);
         element.innerHTML += "<br>Click to close";
         caller.parentNode.appendChild(element);
+
+        // Show the QRCode
+        qrcodeImage = document.getElementById('permalinkQrcode');
+        // Workaround to deal with newly created element lag for transition.
+        window.getComputedStyle(qrcodeImage).opacity;
+        qrcodeImage.className = 'show';
     }
     else
     {
@@ -48,7 +54,7 @@ function showQrCode(caller,loading)
 // Remove any displayed QR-Code
 function removeQrcode()
 {
-    var elem = document.getElementById("permalinkQrcode");
+    var elem = document.getElementById('permalinkQrcode');
     if (elem) {
         elem.parentNode.removeChild(elem);
     }
index 08e0d44a0921e7af96b1dd3e3ee883812cd4f8bd..5bc35be11a847fa9cf93620f3446a517bc5d2aac 100644 (file)
@@ -2,7 +2,8 @@
 
 For each link in your Shaarli, adds a button to save the target page in your [wallabag](https://www.wallabag.org/).
 
-### Installation/configuration
+### Installation
+
 Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there.  
 The directory structure should look like:
 
@@ -11,19 +12,31 @@ The directory structure should look like:
     â””── plugins
      Â Â  â””── wallabag
      Â Â      â”œâ”€â”€ README.md
+            â”œâ”€â”€ config.php.dist
      Â Â      â”œâ”€â”€ wallabag.html
+     Â Â      â”œâ”€â”€ wallabag.php
      Â Â      â””── wallabag.png
 ```
 
-To enable the plugin, add `'wallabag'` to your list of enabled plugins in `data/options.php` (`PLUGINS` array)
-This should look like:
+To enable the plugin, add `'wallabag'` to your list of enabled plugins in `data/options.php` (`PLUGINS` array).
+This should look like:
 
 ```
 $GLOBALS['config']['PLUGINS'] = array('qrcode', 'any_other_plugin', 'wallabag')
 ```
 
-Then, set the `WALLABAG_URL` variable in `data/options.php` pointing to your wallabag URL. Example:
+### Configuration
+
+Copy `config.php.dist` into `config.php` and setup your instance.
 
+*Wallabag instance URL*
 ```
-$GLOBALS['config']['WALLABAG_URL'] = 'http://demo.wallabag.org' ; //Base URL of your wallabag installation
-```
\ No newline at end of file
+$GLOBALS['config']['WALLABAG_URL'] = 'http://v2.wallabag.org' ;
+```
+
+*Wallabag version*: either `1` (for 1.x) or `2` (for 2.x)
+```
+$GLOBALS['config']['WALLABAG_VERSION'] = 2;
+```
+
+> Note: these settings can also be set in `data/config.php`.
\ No newline at end of file
diff --git a/plugins/wallabag/WallabagInstance.php b/plugins/wallabag/WallabagInstance.php
new file mode 100644 (file)
index 0000000..72cc2e5
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * Class WallabagInstance.
+ */
+class WallabagInstance
+{
+    /**
+     * @var array Static reference to differrent WB API versions.
+     *          - key: version ID, must match plugin settings.
+     *          - value: version name.
+     */
+    private static $wallabagVersions = array(
+        1 => '1.x',
+        2 => '2.x',
+    );
+
+    /**
+     * @var array Static reference to WB endpoint according to the API version.
+     *          - key: version name.
+     *          - value: endpoint.
+     */
+    private static $wallabagEndpoints = array(
+        '1.x' => '?plainurl=',
+        '2.x' => 'bookmarklet?url=',
+    );
+
+    /**
+     * @var string Wallabag user instance URL.
+     */
+    private $instanceUrl;
+
+    /**
+     * @var string Wallabag user instance API version.
+     */
+    private $apiVersion;
+
+    function __construct($instance, $version)
+    {
+        if ($this->isVersionAllowed($version)) {
+            $this->apiVersion = self::$wallabagVersions[$version];
+        } else {
+            // Default API version: 1.x.
+            $this->apiVersion = self::$wallabagVersions[1];
+        }
+
+        $this->instanceUrl = add_trailing_slash($instance);
+    }
+
+    /**
+     * Build the Wallabag URL to reach from instance URL and API version endpoint.
+     *
+     * @return string wallabag url.
+     */
+    public function getWallabagUrl()
+    {
+        return $this->instanceUrl . self::$wallabagEndpoints[$this->apiVersion];
+    }
+
+    /**
+     * Checks version configuration.
+     *
+     * @param mixed $version given version ID.
+     *
+     * @return bool true if it's valid, false otherwise.
+     */
+    private function isVersionAllowed($version)
+    {
+        return isset(self::$wallabagVersions[$version]);
+    }
+}
index 7cf0d303516c075d4dfcaaeb54b433bc0ee890e8..a602708f0babeb8e76e3971a8347013f00fad64f 100644 (file)
@@ -1,3 +1,4 @@
 <?php
 
-$GLOBALS['plugins']['WALLABAG_URL'] = 'https://demo.wallabag.org/';
\ No newline at end of file
+$GLOBALS['plugins']['WALLABAG_URL'] = 'https://demo.wallabag.org';
+$GLOBALS['plugins']['WALLABAG_VERSION'] = 1;
\ No newline at end of file
index ddcf8126c56d4bf66d9dd1372eec9f78eefd01f5..d0382adc95b1fab36916b6fb516078a04eda3875 100644 (file)
@@ -1 +1 @@
-<span><a href="%s/?plainurl=%s" target="_blank"><img width="13" height="13" src="%s/wallabag/wallabag.png" title="Save to wallabag" /></a></span>
+<span><a href="%s%s" target="_blank"><img width="13" height="13" src="%s/wallabag/wallabag.png" title="Save to wallabag" /></a></span>
index 37969c976d780e71cb5b78c508d060d98476a356..e3c399a9ab5d85a124db5f99a1f0a135f8fbbbc8 100644 (file)
@@ -4,6 +4,8 @@
  * Plugin Wallabag.
  */
 
+require_once 'WallabagInstance.php';
+
 // don't raise unnecessary warnings
 if (is_file(PluginManager::$PLUGINS_PATH . '/wallabag/config.php')) {
     include PluginManager::$PLUGINS_PATH . '/wallabag/config.php';
@@ -28,12 +30,23 @@ function hook_wallabag_render_linklist($data)
         return $data;
     }
 
-    $wallabag_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
+    $version = isset($GLOBALS['plugins']['WALLABAG_VERSION'])
+        ? $GLOBALS['plugins']['WALLABAG_VERSION']
+        : '';
+    $wallabagInstance = new WallabagInstance($GLOBALS['plugins']['WALLABAG_URL'], $version);
+
+    $wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
 
     foreach ($data['links'] as &$value) {
-        $wallabag = sprintf($wallabag_html, $GLOBALS['plugins']['WALLABAG_URL'], $value['url'], PluginManager::$PLUGINS_PATH);
+        $wallabag = sprintf(
+            $wallabagHtml,
+            $wallabagInstance->getWallabagUrl(),
+            urlencode($value['url']),
+            PluginManager::$PLUGINS_PATH
+        );
         $value['link_plugin'][] = $wallabag;
     }
 
     return $data;
 }
+
index 11ad87d7c52814896a0ca255b2e33d9a5b2a3d08..fe5f3896dfd2cbcf6d87ed3f5998203b508d4615 100644 (file)
@@ -1 +1 @@
-<?php /* 0.6.1 */ ?>
+<?php /* 0.6.2 */ ?>
index 76092b80edff587c869f8b908d58c0028677b079..fd29350534e6675dd7469526607b6dbbe495f34d 100644 (file)
@@ -6,7 +6,7 @@
 require_once 'application/HttpUtils.php';
 
 /**
- * Unitary tests for get_http_url()
+ * Unitary tests for get_http_response()
  */
 class GetHttpUrlTest extends PHPUnit_Framework_TestCase
 {
@@ -15,12 +15,15 @@ class GetHttpUrlTest extends PHPUnit_Framework_TestCase
      */
     public function testGetInvalidLocalUrl()
     {
-        list($headers, $content) = get_http_url('/non/existent', 1);
-        $this->assertEquals('HTTP Error', $headers[0]);
-        $this->assertRegexp(
-            '/failed to open stream: No such file or directory/',
-            $content
-        );
+        // Local
+        list($headers, $content) = get_http_response('/non/existent', 1);
+        $this->assertEquals('Invalid HTTP Url', $headers[0]);
+        $this->assertFalse($content);
+
+        // Non HTTP
+        list($headers, $content) = get_http_response('ftp://save.tld/mysave', 1);
+        $this->assertEquals('Invalid HTTP Url', $headers[0]);
+        $this->assertFalse($content);
     }
 
     /**
@@ -28,11 +31,8 @@ class GetHttpUrlTest extends PHPUnit_Framework_TestCase
      */
     public function testGetInvalidRemoteUrl()
     {
-        list($headers, $content) = get_http_url('http://non.existent', 1);
-        $this->assertEquals('HTTP Error', $headers[0]);
-        $this->assertRegexp(
-            '/Name or service not known/',
-            $content
-        );
+        list($headers, $content) = @get_http_response('http://non.existent', 1);
+        $this->assertFalse($headers);
+        $this->assertFalse($content);
     }
 }
index 7b22b2704f8ba84f3e26995f36c84d071b71161d..3b1a20572a75d428724c8dcf37f50a0f1edb4ff7 100644 (file)
@@ -302,236 +302,49 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * 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
-     * @expectedException              Exception
-     * @expectedExceptionMessageRegExp /Invalid date format/
-     */
-    public function testFilterInvalidDayWithChars()
-    {
-        self::$privateLinkDB->filterDay('Rainy day, dream away');
-    }
-
-    /**
-     * Use an invalid date format
-     * @expectedException              Exception
-     * @expectedExceptionMessageRegExp /Invalid date format/
-     */
-    public function testFilterInvalidDayDigits()
-    {
-        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
+     * Test real_url without redirector.
      */
-    public function testFilterFullTextTitle()
+    public function testLinkRealUrlWithoutRedirector()
     {
-        // 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'))
-        );
+        $db = new LinkDB(self::$testDatastore, false, false);
+        foreach($db as $link) {
+            $this->assertEquals($link['url'], $link['real_url']);
+        }
     }
 
     /**
-     * Full-text search - result from the link's description only
+     * Test real_url with redirector.
      */
-    public function testFilterFullTextDescription()
+    public function testLinkRealUrlWithRedirector()
     {
-        $this->assertEquals(
-            1,
-            sizeof(self::$publicLinkDB->filterFullText('media publishing'))
-        );
+        $redirector = 'http://redirector.to?';
+        $db = new LinkDB(self::$testDatastore, false, false, $redirector);
+        foreach($db as $link) {
+            $this->assertStringStartsWith($redirector, $link['real_url']);
+        }
     }
 
     /**
-     * Full-text search - result from the link's tags only
+     * Test filter with string.
      */
-    public function testFilterFullTextTags()
+    public function testFilterString()
     {
+        $tags = 'dev cartoon';
         $this->assertEquals(
             2,
-            sizeof(self::$publicLinkDB->filterFullText('gnu'))
+            count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false))
         );
     }
 
     /**
-     * Full-text search - result set from mixed sources
+     * Test filter with string.
      */
-    public function testFilterFullTextMixed()
+    public function testFilterArray()
     {
+        $tags = array('dev', 'cartoon');
         $this->assertEquals(
             2,
-            sizeof(self::$publicLinkDB->filterFullText('free software'))
+            count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false))
         );
     }
-
-    /**
-     * Test real_url without redirector.
-     */
-    public function testLinkRealUrlWithoutRedirector()
-    {
-        $db = new LinkDB(self::$testDatastore, false, false);
-        foreach($db as $link) {
-            $this->assertEquals($link['url'], $link['real_url']);
-        }
-    }
-
-    /**
-     * Test real_url with redirector.
-     */
-    public function testLinkRealUrlWithRedirector()
-    {
-        $redirector = 'http://redirector.to?';
-        $db = new LinkDB(self::$testDatastore, false, false, $redirector);
-        foreach($db as $link) {
-            $this->assertStringStartsWith($redirector, $link['real_url']);
-        }
-    }
 }
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php
new file mode 100644 (file)
index 0000000..5107ab7
--- /dev/null
@@ -0,0 +1,242 @@
+<?php
+
+require_once 'application/LinkFilter.php';
+
+/**
+ * Class LinkFilterTest.
+ */
+class LinkFilterTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var LinkFilter instance.
+     */
+    protected static $linkFilter;
+
+    /**
+     * Instanciate linkFilter with ReferenceLinkDB data.
+     */
+    public static function setUpBeforeClass()
+    {
+        $refDB = new ReferenceLinkDB();
+        self::$linkFilter = new LinkFilter($refDB->getLinks());
+    }
+
+    /**
+     * Blank filter.
+     */
+    public function testFilter()
+    {
+        $this->assertEquals(
+            6,
+            count(self::$linkFilter->filter('', ''))
+        );
+
+        // Private only.
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter('', '', false, true))
+        );
+    }
+
+    /**
+     * Filter links using a tag
+     */
+    public function testFilterOneTag()
+    {
+        $this->assertEquals(
+            4,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false))
+        );
+
+        // Private only.
+        $this->assertEquals(
+            1,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'web', false, true))
+        );
+    }
+
+    /**
+     * Filter links using a tag - case-sensitive
+     */
+    public function testFilterCaseSensitiveTag()
+    {
+        $this->assertEquals(
+            0,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'mercurial', true))
+        );
+
+        $this->assertEquals(
+            1,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'Mercurial', true))
+        );
+    }
+
+    /**
+     * Filter links using a tag combination
+     */
+    public function testFilterMultipleTags()
+    {
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'dev cartoon', false))
+        );
+    }
+
+    /**
+     * Filter links using a non-existent tag
+     */
+    public function testFilterUnknownTag()
+    {
+        $this->assertEquals(
+            0,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'null', false))
+        );
+    }
+
+    /**
+     * Return links for a given day
+     */
+    public function testFilterDay()
+    {
+        $this->assertEquals(
+            3,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20121206'))
+        );
+    }
+
+    /**
+     * 404 - day not found
+     */
+    public function testFilterUnknownDay()
+    {
+        $this->assertEquals(
+            0,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '19700101'))
+        );
+    }
+
+    /**
+     * Use an invalid date format
+     * @expectedException              Exception
+     * @expectedExceptionMessageRegExp /Invalid date format/
+     */
+    public function testFilterInvalidDayWithChars()
+    {
+        self::$linkFilter->filter(LinkFilter::$FILTER_DAY, 'Rainy day, dream away');
+    }
+
+    /**
+     * Use an invalid date format
+     * @expectedException              Exception
+     * @expectedExceptionMessageRegExp /Invalid date format/
+     */
+    public function testFilterInvalidDayDigits()
+    {
+        self::$linkFilter->filter(LinkFilter::$FILTER_DAY, '20');
+    }
+
+    /**
+     * Retrieve a link entry with its hash
+     */
+    public function testFilterSmallHash()
+    {
+        $links = self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'IuWvgA');
+
+        $this->assertEquals(
+            1,
+            count($links)
+        );
+
+        $this->assertEquals(
+            'MediaGoblin',
+            $links['20130614_184135']['title']
+        );
+    }
+
+    /**
+     * No link for this hash
+     */
+    public function testFilterUnknownSmallHash()
+    {
+        $this->assertEquals(
+            0,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_HASH, 'Iblaah'))
+        );
+    }
+
+    /**
+     * Full-text search - result from a link's URL
+     */
+    public function testFilterFullTextURL()
+    {
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'ars.userfriendly.org'))
+        );
+    }
+
+    /**
+     * Full-text search - result from a link's title only
+     */
+    public function testFilterFullTextTitle()
+    {
+        // use miscellaneous cases
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'userfriendly -'))
+        );
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'UserFriendly -'))
+        );
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'uSeRFrIendlY -'))
+        );
+
+        // use miscellaneous case and offset
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'RFrIendL'))
+        );
+    }
+
+    /**
+     * Full-text search - result from the link's description only
+     */
+    public function testFilterFullTextDescription()
+    {
+        $this->assertEquals(
+            1,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'media publishing'))
+        );
+    }
+
+    /**
+     * Full-text search - result from the link's tags only
+     */
+    public function testFilterFullTextTags()
+    {
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'gnu'))
+        );
+
+        // Private only.
+        $this->assertEquals(
+            1,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'web', false, true))
+        );
+    }
+
+    /**
+     * Full-text search - result set from mixed sources
+     */
+    public function testFilterFullTextMixed()
+    {
+        $this->assertEquals(
+            2,
+            count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software'))
+        );
+    }
+}
diff --git a/tests/LinkUtilsTest.php b/tests/LinkUtilsTest.php
new file mode 100644 (file)
index 0000000..c225759
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+
+require_once 'application/LinkUtils.php';
+
+/**
+* Class LinkUtilsTest.
+*/
+class LinkUtilsTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Test html_extract_title() when the title is found.
+     */
+    public function testHtmlExtractExistentTitle()
+    {
+        $title = 'Read me please.';
+        $html = '<html><meta>stuff</meta><title>'. $title .'</title></html>';
+        $this->assertEquals($title, html_extract_title($html));
+    }
+
+    /**
+     * Test html_extract_title() when the title is not found.
+     */
+    public function testHtmlExtractNonExistentTitle()
+    {
+        $html = '<html><meta>stuff</meta></html>';
+        $this->assertFalse(html_extract_title($html));
+    }
+
+    /**
+     * Test get_charset() with all priorities.
+     */
+    public function testGetCharset()
+    {
+        $headers = array('Content-Type' => 'text/html; charset=Headers');
+        $html = '<html><meta>stuff</meta><meta charset="Html"/></html>';
+        $default = 'default';
+        $this->assertEquals('headers', get_charset($headers, $html, $default));
+        $this->assertEquals('html', get_charset(array(), $html, $default));
+        $this->assertEquals($default, get_charset(array(), '', $default));
+        $this->assertEquals('utf-8', get_charset(array(), ''));
+    }
+
+    /**
+     * Test headers_extract_charset() when the charset is found.
+     */
+    public function testHeadersExtractExistentCharset()
+    {
+        $charset = 'x-MacCroatian';
+        $headers = array('Content-Type' => 'text/html; charset='. $charset);
+        $this->assertEquals(strtolower($charset), headers_extract_charset($headers));
+    }
+
+    /**
+     * Test headers_extract_charset() when the charset is not found.
+     */
+    public function testHeadersExtractNonExistentCharset()
+    {
+        $headers = array();
+        $this->assertFalse(headers_extract_charset($headers));
+
+        $headers = array('Content-Type' => 'text/html');
+        $this->assertFalse(headers_extract_charset($headers));
+    }
+
+    /**
+     * Test html_extract_charset() when the charset is found.
+     */
+    public function testHtmlExtractExistentCharset()
+    {
+        $charset = 'x-MacCroatian';
+        $html = '<html><meta>stuff2</meta><meta charset="'. $charset .'"/></html>';
+        $this->assertEquals(strtolower($charset), html_extract_charset($html));
+    }
+
+    /**
+     * Test html_extract_charset() when the charset is not found.
+     */
+    public function testHtmlExtractNonExistentCharset()
+    {
+        $html = '<html><meta>stuff</meta></html>';
+        $this->assertFalse(html_extract_charset($html));
+        $html = '<html><meta>stuff</meta><meta charset=""/></html>';
+        $this->assertFalse(html_extract_charset($html));
+    }
+}
index e498d79e8ed8dbf9057ce88c04fd1e98143a5408..425327ed02b37b7f1ccb638208ddf404ed7b16d5 100644 (file)
@@ -145,4 +145,33 @@ class UrlTest extends PHPUnit_Framework_TestCase
         $url = new Url('git://domain.tld/push?pull=clone#checkout');
         $this->assertEquals('git', $url->getScheme());
     }
+
+    /**
+     * Test add trailing slash.
+     */
+    function testAddTrailingSlash()
+    {
+        $strOn = 'http://randomstr.com/test/';
+        $strOff = 'http://randomstr.com/test';
+        $this->assertEquals($strOn, add_trailing_slash($strOn));
+        $this->assertEquals($strOn, add_trailing_slash($strOff));
+    }
+
+    /**
+     * Test valid HTTP url.
+     */
+    function testUrlIsHttp()
+    {
+        $url = new Url(self::$baseUrl);
+        $this->assertTrue($url->isHttp());
+    }
+
+    /**
+     * Test non HTTP url.
+     */
+    function testUrlIsNotHttp()
+    {
+        $url = new Url('ftp://save.tld/mysave');
+        $this->assertFalse($url->isHttp());
+    }
 }
index 02eecda216f8c03adafe50f57bcb32bdf7ace787..3073b5eb45ca2736f7ed8cf649ad9997392e1287 100644 (file)
@@ -18,6 +18,13 @@ class UtilsTest extends PHPUnit_Framework_TestCase
     // Session ID hashes
     protected static $sidHashes = null;
 
+    // Log file
+    protected static $testLogFile = 'tests.log';
+
+    // Expected log date format
+    protected static $dateFormat = 'Y/m/d H:i:s';
+    
+
     /**
      * Assign reference data
      */
@@ -26,6 +33,65 @@ class UtilsTest extends PHPUnit_Framework_TestCase
         self::$sidHashes = ReferenceSessionIdHashes::getHashes();
     }
 
+    /**
+     * Resets test data before each test
+     */
+    protected function setUp()
+    {
+        if (file_exists(self::$testLogFile)) {
+            unlink(self::$testLogFile);
+        }
+    }
+
+    /**
+     * Returns a list of the elements from the last logged entry
+     *
+     * @return list (date, ip address, message)
+     */
+    protected function getLastLogEntry()
+    {
+        $logFile = file(self::$testLogFile);
+        return explode(' - ', trim(array_pop($logFile), PHP_EOL));
+    }
+
+    /**
+     * Log a message to a file - IPv4 client address
+     */
+    public function testLogmIp4()
+    {
+        $logMessage = 'IPv4 client connected';
+        logm(self::$testLogFile, '127.0.0.1', $logMessage);
+        list($date, $ip, $message) = $this->getLastLogEntry();
+
+        $this->assertInstanceOf(
+            'DateTime',
+            DateTime::createFromFormat(self::$dateFormat, $date)
+        );
+        $this->assertTrue(
+            filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false
+        );
+        $this->assertEquals($logMessage, $message);
+    }
+
+    /**
+     * Log a message to a file - IPv6 client address
+     */
+    public function testLogmIp6()
+    {
+        $logMessage = 'IPv6 client connected';
+        logm(self::$testLogFile, '2001:db8::ff00:42:8329', $logMessage);
+        list($date, $ip, $message) = $this->getLastLogEntry();
+
+        $this->assertInstanceOf(
+            'DateTime',
+            DateTime::createFromFormat(self::$dateFormat, $date)
+        );
+        $this->assertTrue(
+            filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false
+        );
+        $this->assertEquals($logMessage, $message);
+    }
+
     /**
      * Represent a link by its hash
      */
index c749fa86f6a49aa167bb4c8c7761a2ce32b38f8c..86dc7f293059a94830f0cbec3f0f7cc74229becc 100644 (file)
@@ -30,7 +30,7 @@ class PlugQrcodeTest extends PHPUnit_Framework_TestCase
             'title' => $str,
             'links' => array(
                 array(
-                    'real_url' => $str,
+                    'url' => $str,
                 )
             )
         );
@@ -39,7 +39,7 @@ class PlugQrcodeTest extends PHPUnit_Framework_TestCase
         $link = $data['links'][0];
         // data shouldn't be altered
         $this->assertEquals($str, $data['title']);
-        $this->assertEquals($str, $link['real_url']);
+        $this->assertEquals($str, $link['url']);
 
         // plugin data
         $this->assertEquals(1, count($link['link_plugin']));
index 7cc83f4f7fc2515e41e1a40bc253a2c91d04eb1e..5d3a60e02d109fec041f433070a38a8b2f6fede3 100644 (file)
@@ -44,6 +44,8 @@ class PluginWallabagTest extends PHPUnit_Framework_TestCase
 
         // plugin data
         $this->assertEquals(1, count($link['link_plugin']));
-        $this->assertNotFalse(strpos($link['link_plugin'][0], $str));
+        $this->assertNotFalse(strpos($link['link_plugin'][0], urlencode($str)));
+        $this->assertNotFalse(strpos($link['link_plugin'][0], $GLOBALS['plugins']['WALLABAG_URL']));
     }
 }
+
diff --git a/tests/plugins/WallabagInstanceTest.php b/tests/plugins/WallabagInstanceTest.php
new file mode 100644 (file)
index 0000000..7c14c1d
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+require_once 'plugins/wallabag/WallabagInstance.php';
+
+/**
+ * Class WallabagInstanceTest
+ */
+class WallabagInstanceTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var string wallabag url.
+     */
+    private $instance;
+
+    /**
+     * Reset plugin path
+     */
+    function setUp()
+    {
+        $this->instance = 'http://some.url';
+    }
+
+    /**
+     * Test WallabagInstance with API V1.
+     */
+    function testWallabagInstanceV1()
+    {
+        $instance = new WallabagInstance($this->instance, 1);
+        $expected = $this->instance . '/?plainurl=';
+        $result = $instance->getWallabagUrl();
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test WallabagInstance with API V2.
+     */
+    function testWallabagInstanceV2()
+    {
+        $instance = new WallabagInstance($this->instance, 2);
+        $expected = $this->instance . '/bookmarklet?url=';
+        $result = $instance->getWallabagUrl();
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * Test WallabagInstance with an invalid API version.
+     */
+    function testWallabagInstanceInvalidVersion()
+    {
+        $instance = new WallabagInstance($this->instance, false);
+        $expected = $this->instance . '/?plainurl=';
+        $result = $instance->getWallabagUrl();
+        $this->assertEquals($expected, $result);
+
+        $instance = new WallabagInstance($this->instance, 3);
+        $expected = $this->instance . '/?plainurl=';
+        $result = $instance->getWallabagUrl();
+        $this->assertEquals($expected, $result);
+    }
+}
index 47b51829989018906824c0cdc34b2f154764b0bc..011317ef970eb3ea9834ebc8b3834a04e0a00435 100644 (file)
@@ -124,4 +124,9 @@ class ReferenceLinkDB
     {
         return $this->_privateCount;
     }
+
+    public function getLinks()
+    {
+        return $this->_links;
+    }
 }
diff --git a/tpl/404.html b/tpl/404.html
new file mode 100644 (file)
index 0000000..53e98e2
--- /dev/null
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+    {include="includes"}
+</head>
+<body>
+<div id="pageheader">
+    {include="page.header"}
+</div>
+<div class="error-container">
+    <h1>404 Not found <small>Oh crap!</small></h1>
+    <p>{$error_message}</p>
+    <p>Would you mind <a href="?">clicking here</a>?</p>
+</div>
+{include="page.footer"}
+</body>
+</html>
index 666748a7b3f2d4bf660ee116ed016af9b2dc32a3..ca91699ee81ff1ede7c4015d4803656dbe787cad 100644 (file)
@@ -7,15 +7,24 @@
 <body>
 <div id="pageheader">
     {include="page.header"}
+
     <div id="headerform" class="search">
         <form method="GET" class="searchform" name="searchform">
-            <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text" value="">
+            <input type="text" tabindex="1" id="searchform_value" name="searchterm" placeholder="Search text"
+               {if="!empty($search_crits) && $search_type=='fulltext'"}
+                    value="{$search_crits}"
+               {/if}
+            >
             <input type="submit" value="Search" class="bigbutton">
         </form>
         <form method="GET" class="tagfilter" name="tagfilter">
-            <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag" value=""
-                   autocomplete="off" class="awesomplete" data-multiple data-minChars="1"
-                   data-list="{loop="$tags"}{$key}, {/loop}">
+            <input type="text" tabindex="2" name="searchtags" id="tagfilter_value" placeholder="Filter by tag"
+                {if="!empty($search_crits) && $search_type=='tags'"}
+                    value="{function="implode(' ', $search_crits)"}"
+                {/if}
+                autocomplete="off" class="awesomplete" data-multiple data-minChars="1"
+                data-list="{loop="$tags"}{$key}, {/loop}"
+            >
             <input type="submit" value="Search" class="bigbutton">
         </form>
         {loop="$plugins_header.fields_toolbar"}
@@ -44,7 +53,7 @@
             <div id="searchcriteria">{$result_count} results for tags <i>
             {loop="search_crits"}
                 <span class="linktag" title="Remove tag">
-                    <a href="?removetag={$value}">{$value} <span class="remove">x</span></a>
+                    <a href="?removetag={function="urlencode($value)"}">{$value} <span class="remove">x</span></a>
                 </span>
             {/loop}</i></div>
         {/if}
old mode 100755 (executable)
new mode 100644 (file)