diff --git a/application/LinkDB.php b/application/LinkDB.php index a95b3f36..416aa0d3 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -340,7 +340,7 @@ public function getLinkFromUrl($url) * * @return array filtered links */ - public function filter($type, $request, $casesensitive = false, $privateonly = false) + public function filter($type = '', $request = '', $casesensitive = false, $privateonly = false) { $linkFilter = new LinkFilter($this->_links); $requestFilter = is_array($request) ? implode(' ', $request) : $request; diff --git a/application/LinkFilter.php b/application/LinkFilter.php index 096d3b04..ceb47d16 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php @@ -209,19 +209,33 @@ private function filterFulltext($searchterms, $privateonly = false) */ public function filterTags($tags, $casesensitive = false, $privateonly = false) { - $searchtags = $this->tagsStrToArray($tags, $casesensitive); + $searchtags = self::tagsStrToArray($tags, $casesensitive); $filtered = array(); + if (empty($searchtags)) { + return $filtered; + } - foreach ($this->links as $l) { + foreach ($this->links as $link) { // ignore non private links when 'privatonly' is on. - if (! $l['private'] && $privateonly === true) { + if (! $link['private'] && $privateonly === true) { continue; } - $linktags = $this->tagsStrToArray($l['tags'], $casesensitive); + $linktags = self::tagsStrToArray($link['tags'], $casesensitive); - if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) { - $filtered[$l['linkdate']] = $l; + $found = true; + for ($i = 0 ; $i < count($searchtags) && $found; $i++) { + // Exclusive search, quit if tag found. + // Or, tag not found in the link, quit. + if (($searchtags[$i][0] == '-' && in_array(substr($searchtags[$i], 1), $linktags)) + || ($searchtags[$i][0] != '-') && ! in_array($searchtags[$i], $linktags) + ) { + $found = false; + } + } + + if ($found) { + $filtered[$link['linkdate']] = $link; } } krsort($filtered); @@ -266,12 +280,12 @@ public function filterDay($day) * * @return array filtered tags string. */ - public function tagsStrToArray($tags, $casesensitive) + public static 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)); + return array_filter(explode(' ', trim($tagsOut)), 'strlen'); } } diff --git a/application/Updater.php b/application/Updater.php index 20ae0c4d..773a1ffa 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -131,6 +131,21 @@ public function updateMethodMergeDeprecatedConfigFile() return true; } + + /** + * Rename tags starting with a '-' to work with tag exclusion search. + */ + public function updateMethodRenameDashTags() + { + $linklist = $this->linkDB->filter(); + foreach ($linklist as $link) { + $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); + $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true))); + $this->linkDB[$link['linkdate']] = $link; + } + $this->linkDB->savedb($this->config['config']['PAGECACHE']); + return true; + } } /** diff --git a/index.php b/index.php index 4382bd80..d9fe5bc2 100644 --- a/index.php +++ b/index.php @@ -1558,6 +1558,8 @@ function renderPage() } // Remove multiple spaces. $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags'])); + // Remove first '-' char in tags. + $tags = preg_replace('/(^| )\-/', '$1', $tags); // Remove duplicates. $tags = implode(' ', array_unique(explode(' ', $tags))); $linkdate = $_POST['lf_linkdate']; diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 3b1a2057..06edea79 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php @@ -276,7 +276,8 @@ public function testAllTags() 'media' => 1, 'software' => 1, 'stallman' => 1, - 'free' => 1 + 'free' => 1, + '-exclude' => 1, ), self::$publicLinkDB->allTags() ); @@ -295,7 +296,8 @@ public function testAllTags() 'html' => 1, 'w3c' => 1, 'css' => 1, - 'Mercurial' => 1 + 'Mercurial' => 1, + '-exclude' => 1, ), self::$privateLinkDB->allTags() ); diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php index 5fb2423f..164af0d4 100644 --- a/tests/LinkFilterTest.php +++ b/tests/LinkFilterTest.php @@ -254,4 +254,20 @@ public function testFilterFullTextMixed() count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software')) ); } + + /** + * Tag search with exclusion. + */ + public function testTagFilterWithExclusion() + { + $this->assertEquals( + 1, + count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'gnu -free')) + ); + + $this->assertEquals( + 5, + count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) + ); + } } diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php index 63ed5e03..84b82350 100644 --- a/tests/Updater/UpdaterTest.php +++ b/tests/Updater/UpdaterTest.php @@ -13,6 +13,11 @@ class UpdaterTest extends PHPUnit_Framework_TestCase */ private static $configFields; + /** + * @var string Path to test datastore. + */ + protected static $testDatastore = 'sandbox/datastore.php'; + /** * Executed before each test. */ @@ -31,6 +36,7 @@ public function setUp() 'config' => array( 'CONFIG_FILE' => 'tests/Updater/config.php', 'DATADIR' => 'tests/Updater', + 'PAGECACHE' => 'sandbox/pagecache', 'config1' => 'config1data', 'config2' => 'config2data', ) @@ -224,4 +230,16 @@ public function testMergeDeprecatedConfigNoFile() include self::$configFields['config']['CONFIG_FILE']; $this->assertEquals(self::$configFields['login'], $GLOBALS['login']); } + + public function testRenameDashTags() + { + $refDB = new ReferenceLinkDB(); + $refDB->write(self::$testDatastore); + $linkDB = new LinkDB(self::$testDatastore, true, false); + $this->assertEmpty($linkDB->filter(LinkFilter::$FILTER_TAG, 'exclude')); + $updater = new Updater(array(), self::$configFields, $linkDB, true); + $updater->updateMethodRenameDashTags(); + var_dump($linkDB->filter(LinkFilter::$FILTER_TAG, 'exclude')); + $this->assertNotEmpty($linkDB->filter(LinkFilter::$FILTER_TAG, 'exclude')); + } } diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index 011317ef..2f188d29 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php @@ -19,7 +19,7 @@ function __construct() 'Richard Stallman and the Free Software Revolution', 0, '20150310_114633', - 'free gnu software stallman' + 'free gnu software stallman -exclude' ); $this->addLink(