From: ArthurHoaro Date: Thu, 25 May 2017 13:54:20 +0000 (+0200) Subject: Merge pull request #841 from ArthurHoaro/feature/search-no-tag X-Git-Tag: v0.9.1~1^2~27 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=3e395a6bc63cba16b0772a382f6cbac0ce4ab906;hp=-c;p=github%2Fshaarli%2FShaarli.git Merge pull request #841 from ArthurHoaro/feature/search-no-tag Empty tag search will look for not tagged links --- 3e395a6bc63cba16b0772a382f6cbac0ce4ab906 diff --combined application/LinkDB.php index 7802cc8a,a03c2c06..8ca0fab3 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@@ -50,6 -50,12 +50,6 @@@ class LinkDB implements Iterator, Count // Link date storage format const LINK_DATE_FORMAT = 'Ymd_His'; - // Datastore PHP prefix - protected static $phpPrefix = ''; - // List of links (associative array) // - key: link date (e.g. "20110823_124546"), // - value: associative array (keys: title, description...) @@@ -138,10 -144,10 +138,10 @@@ if (!isset($value['id']) || empty($value['url'])) { die('Internal Error: A link should always have an id and URL.'); } - if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { + if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { die('You must specify an integer as a key.'); } - if (! empty($offset) && $offset !== $value['id']) { + if ($offset !== null && $offset !== $value['id']) { die('Array offset and link ID must be equal.'); } @@@ -289,7 -295,16 +289,7 @@@ You use the community supported versio return; } - // Read data - // Note that gzinflate is faster than gzuncompress. - // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 - $this->links = array(); - - if (file_exists($this->datastore)) { - $this->links = unserialize(gzinflate(base64_decode( - substr(file_get_contents($this->datastore), - strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); - } + $this->links = FileUtils::readFlatDB($this->datastore, []); $toremove = array(); foreach ($this->links as $key => &$link) { @@@ -346,7 -361,19 +346,7 @@@ */ private function write() { - if (is_file($this->datastore) && !is_writeable($this->datastore)) { - // The datastore exists but is not writeable - throw new IOException($this->datastore); - } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { - // The datastore does not exist and its parent directory is not writeable - throw new IOException(dirname($this->datastore)); - } - - file_put_contents( - $this->datastore, - self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix - ); - + FileUtils::writeFlatDB($this->datastore, $this->links); } /** @@@ -423,46 -450,26 +423,29 @@@ public function filterSearch($filterRequest = array(), $casesensitive = false, $visibility = 'all') { // Filter link database according to parameters. - $searchtags = !empty($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; - $searchterm = !empty($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; + $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; + $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; - // Search tags + fullsearch. - if (! empty($searchtags) && ! empty($searchterm)) { - $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; - $request = array($searchtags, $searchterm); - } - // Search by tags. - elseif (! empty($searchtags)) { - $type = LinkFilter::$FILTER_TAG; - $request = $searchtags; - } - // Fulltext search. - elseif (! empty($searchterm)) { - $type = LinkFilter::$FILTER_TEXT; - $request = $searchterm; - } - // Otherwise, display without filtering. - else { - $type = ''; - $request = ''; - } + // Search tags + fullsearch - blank string parameter will return all links. + $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; + $request = [$searchtags, $searchterm]; $linkFilter = new LinkFilter($this); return $linkFilter->filter($type, $request, $casesensitive, $visibility); } /** - * Returns the list of all tags - * Output: associative array key=tags, value=0 + * Returns the list tags appearing in the links with the given tags + * @param $filteringTags: tags selecting the links to consider + * @param $visibility: process only all/private/public links + * @return: a tag=>linksCount array */ - public function allTags() + public function linksCountPerTag($filteringTags = [], $visibility = 'all') { + $links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility); $tags = array(); $caseMapping = array(); - foreach ($this->links as $link) { + foreach ($links as $link) { foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { if (empty($tag)) { continue; diff --combined application/Utils.php index 9d0ebc5e,87e5cc8f..4a2f5561 --- a/application/Utils.php +++ b/application/Utils.php @@@ -91,6 -91,10 +91,10 @@@ function endsWith($haystack, $needle, $ */ function escape($input) { + if (is_bool($input)) { + return $input; + } + if (is_array($input)) { $out = array(); foreach($input as $key => $value) { @@@ -321,148 -325,25 +325,148 @@@ function normalize_spaces($string * otherwise default format '%c' will be returned. * * @param DateTime $date to format. + * @param bool $time Displays time if true. * @param bool $intl Use international format if true. * * @return bool|string Formatted date, or false if the input is invalid. */ -function format_date($date, $intl = true) +function format_date($date, $time = true, $intl = true) { if (! $date instanceof DateTime) { return false; } if (! $intl || ! class_exists('IntlDateFormatter')) { - return strftime('%c', $date->getTimestamp()); + $format = $time ? '%c' : '%x'; + return strftime($format, $date->getTimestamp()); } $formatter = new IntlDateFormatter( setlocale(LC_TIME, 0), IntlDateFormatter::LONG, - IntlDateFormatter::LONG + $time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE ); return $formatter->format($date); } + +/** + * Check if the input is an integer, no matter its real type. + * + * PHP is a bit messy regarding this: + * - is_int returns false if the input is a string + * - ctype_digit returns false if the input is an integer or negative + * + * @param mixed $input value + * + * @return bool true if the input is an integer, false otherwise + */ +function is_integer_mixed($input) +{ + if (is_array($input) || is_bool($input) || is_object($input)) { + return false; + } + $input = strval($input); + return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1))); +} + +/** + * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes. + * + * @param string $val Size expressed in string. + * + * @return int Size expressed in bytes. + */ +function return_bytes($val) +{ + if (is_integer_mixed($val) || $val === '0' || empty($val)) { + return $val; + } + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + $val = intval(substr($val, 0, -1)); + switch($last) { + case 'g': $val *= 1024; + case 'm': $val *= 1024; + case 'k': $val *= 1024; + } + return $val; +} + +/** + * Return a human readable size from bytes. + * + * @param int $bytes value + * + * @return string Human readable size + */ +function human_bytes($bytes) +{ + if ($bytes === '') { + return t('Setting not set'); + } + if (! is_integer_mixed($bytes)) { + return $bytes; + } + $bytes = intval($bytes); + if ($bytes === 0) { + return t('Unlimited'); + } + + $units = [t('B'), t('kiB'), t('MiB'), t('GiB')]; + for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) { + $bytes /= 1024; + } + + return round($bytes) . $units[$i]; +} + +/** + * Try to determine max file size for uploads (POST). + * Returns an integer (in bytes) or formatted depending on $format. + * + * @param mixed $limitPost post_max_size PHP setting + * @param mixed $limitUpload upload_max_filesize PHP setting + * @param bool $format Format max upload size to human readable size + * + * @return int|string max upload file size + */ +function get_max_upload_size($limitPost, $limitUpload, $format = true) +{ + $size1 = return_bytes($limitPost); + $size2 = return_bytes($limitUpload); + // Return the smaller of two: + $maxsize = min($size1, $size2); + return $format ? human_bytes($maxsize) : $maxsize; +} + +/** + * Sort the given array alphabetically using php-intl if available. + * Case sensitive. + * + * Note: doesn't support multidimensional arrays + * + * @param array $data Input array, passed by reference + * @param bool $reverse Reverse sort if set to true + * @param bool $byKeys Sort the array by keys if set to true, by value otherwise. + */ +function alphabetical_sort(&$data, $reverse = false, $byKeys = false) +{ + $callback = function($a, $b) use ($reverse) { + // Collator is part of PHP intl. + if (class_exists('Collator')) { + $collator = new Collator(setlocale(LC_COLLATE, 0)); + if (!intl_is_failure(intl_get_error_code())) { + return $collator->compare($a, $b) * ($reverse ? -1 : 1); + } + } + + return strcasecmp($a, $b) * ($reverse ? -1 : 1); + }; + + if ($byKeys) { + uksort($data, $callback); + } else { + usort($data, $callback); + } +} diff --combined index.php index 61b71129,c96d0136..92eb443b --- a/index.php +++ b/index.php @@@ -62,7 -62,6 +62,7 @@@ require_once 'application/CachedPage.ph require_once 'application/config/ConfigPlugin.php'; require_once 'application/FeedBuilder.php'; require_once 'application/FileUtils.php'; +require_once 'application/History.php'; require_once 'application/HttpUtils.php'; require_once 'application/Languages.php'; require_once 'application/LinkDB.php'; @@@ -225,6 -224,27 +225,6 @@@ function setup_login_state($conf } $userIsLoggedIn = setup_login_state($conf); -/** - * PubSubHubbub protocol support (if enabled) [UNTESTED] - * (Source: http://aldarone.fr/les-flux-rss-shaarli-et-pubsubhubbub/ ) - * - * @param ConfigManager $conf Configuration Manager instance. - */ -function pubsubhub($conf) -{ - $pshUrl = $conf->get('config.PUBSUBHUB_URL'); - if (!empty($pshUrl)) - { - include_once './publisher.php'; - $p = new Publisher($pshUrl); - $topic_url = array ( - index_url($_SERVER).'?do=atom', - index_url($_SERVER).'?do=rss' - ); - $p->publish_update($topic_url); - } -} - // ------------------------------------------------------------------------------------------ // Session management @@@ -452,6 -472,34 +452,6 @@@ if (isset($_POST['login']) } } -// ------------------------------------------------------------------------------------------ -// Misc utility functions: - -// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes. -function return_bytes($val) -{ - $val = trim($val); $last=strtolower($val[strlen($val)-1]); - switch($last) - { - case 'g': $val *= 1024; - case 'm': $val *= 1024; - case 'k': $val *= 1024; - } - return $val; -} - -// Try to determine max file size for uploads (POST). -// Returns an integer (in bytes) -function getMaxFileSize() -{ - $size1 = return_bytes(ini_get('post_max_size')); - $size2 = return_bytes(ini_get('upload_max_filesize')); - // Return the smaller of two: - $maxsize = min($size1,$size2); - // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000) - return $maxsize; -} - // ------------------------------------------------------------------------------------------ // Token management for XSRF protection // Token should be used in any form which acts on data (create,update,delete,import...). @@@ -647,11 -695,9 +647,11 @@@ function showDaily($pageBuilder, $LINKS $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); $data = array( + 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false), 'linksToDisplay' => $linksToDisplay, 'cols' => $columns, 'day' => $dayDate->getTimestamp(), + 'dayDate' => $dayDate, 'previousday' => $previousday, 'nextday' => $nextday, ); @@@ -686,7 -732,7 +686,7 @@@ function showLinkList($PAGE, $LINKSDB, * @param PluginManager $pluginManager Plugin Manager instance, * @param LinkDB $LINKSDB */ -function renderPage($conf, $pluginManager, $LINKSDB) +function renderPage($conf, $pluginManager, $LINKSDB, $history) { $updater = new Updater( read_updates_file($conf->get('resource.updates')), @@@ -707,7 -753,7 +707,7 @@@ die($e->getMessage()); } - $PAGE = new PageBuilder($conf); + $PAGE = new PageBuilder($conf, $LINKSDB); $PAGE->assign('linkcount', count($LINKSDB)); $PAGE->assign('privateLinkcount', count_private($LINKSDB)); $PAGE->assign('plugin_errors', $pluginManager->getErrors()); @@@ -790,9 -836,7 +790,9 @@@ // -------- Tag cloud if ($targetPage == Router::$PAGE_TAGCLOUD) { - $tags= $LINKSDB->allTags(); + $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; + $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; + $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); // We sort tags alphabetically, then choose a font size according to count. // First, find max value. @@@ -801,7 -845,17 +801,7 @@@ $maxcount = max($maxcount, $value); } - // Sort tags alphabetically: case insensitive, support locale if available. - uksort($tags, function($a, $b) { - // Collator is part of PHP intl. - if (class_exists('Collator')) { - $c = new Collator(setlocale(LC_COLLATE, 0)); - if (!intl_is_failure(intl_get_error_code())) { - return $c->compare($a, $b); - } - } - return strcasecmp($a, $b); - }); + alphabetical_sort($tags, true, true); $tagList = array(); foreach($tags as $key => $value) { @@@ -816,7 -870,6 +816,7 @@@ } $data = array( + 'search_tags' => implode(' ', $filteringTags), 'tags' => $tagList, ); $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); @@@ -825,32 -878,7 +825,32 @@@ $PAGE->assign($key, $value); } - $PAGE->renderPage('tagcloud'); + $PAGE->renderPage('tag.cloud'); + exit; + } + + // -------- Tag cloud + if ($targetPage == Router::$PAGE_TAGLIST) + { + $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; + $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; + $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); + + if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') { + alphabetical_sort($tags, false, true); + } + + $data = [ + 'search_tags' => implode(' ', $filteringTags), + 'tags' => $tags, + ]; + $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); + + foreach ($data as $key => $value) { + $PAGE->assign($key, $value); + } + + $PAGE->renderPage('tag.list'); exit; } @@@ -1123,7 -1151,6 +1123,7 @@@ $conf->set('api.secret', escape($_POST['apiSecret'])); try { $conf->write(isLoggedIn()); + $history->updateSettings(); invalidateCaches($conf->get('resource.page_cache')); } catch(Exception $e) { @@@ -1145,12 -1172,9 +1145,12 @@@ $PAGE->assign('theme', $conf->get('resource.theme')); $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl'))); $PAGE->assign('redirector', $conf->get('redirector.url')); - list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone')); - $PAGE->assign('timezone_form', $timezone_form); - $PAGE->assign('timezone_js',$timezone_js); + list($continents, $cities) = generateTimeZoneData( + timezone_identifiers_list(), + $conf->get('general.timezone') + ); + $PAGE->assign('continents', $continents); + $PAGE->assign('cities', $cities); $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false)); $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false)); $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false)); @@@ -1167,7 -1191,7 +1167,7 @@@ if ($targetPage == Router::$PAGE_CHANGETAG) { if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { - $PAGE->assign('tags', $LINKSDB->allTags()); + $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : ''); $PAGE->renderPage('changetag'); exit; } @@@ -1187,7 -1211,6 +1187,7 @@@ unset($tags[array_search($needle,$tags)]); // Remove tag. $value['tags']=trim(implode(' ',$tags)); $LINKSDB[$key]=$value; + $history->updateLink($LINKSDB[$key]); } $LINKSDB->save($conf->get('resource.page_cache')); echo ''; @@@ -1205,7 -1228,6 +1205,7 @@@ $tags[array_search($needle, $tags)] = trim($_POST['totag']); $value['tags'] = implode(' ', array_unique($tags)); $LINKSDB[$key] = $value; + $history->updateLink($LINKSDB[$key]); } $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk. echo ''; @@@ -1240,13 -1262,11 +1240,13 @@@ $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); $updated = new DateTime(); $shortUrl = $LINKSDB[$id]['shorturl']; + $new = false; } else { // New link $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); $updated = null; $shortUrl = link_small_hash($created, $id); + $new = true; } // Remove multiple spaces. @@@ -1285,11 -1305,6 +1285,11 @@@ $LINKSDB[$id] = $link; $LINKSDB->save($conf->get('resource.page_cache')); + if ($new) { + $history->addLink($link); + } else { + $history->updateLink($link); + } // If we are called from the bookmarklet, we must close the popup: if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { @@@ -1327,23 -1342,19 +1327,23 @@@ // -------- User clicked the "Delete" button when editing a link: Delete link from database. if ($targetPage == Router::$PAGE_DELETELINK) { - // We do not need to ask for confirmation: - // - confirmation is handled by JavaScript - // - we are protected from XSRF by the token. - if (! tokenOk($_GET['token'])) { die('Wrong token.'); } - $id = intval(escape($_GET['lf_linkdate'])); - $link = $LINKSDB[$id]; - $pluginManager->executeHooks('delete_link', $link); - unset($LINKSDB[$id]); + if (strpos($_GET['lf_linkdate'], ' ') !== false) { + $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate'])))); + } else { + $ids = [$_GET['lf_linkdate']]; + } + foreach ($ids as $id) { + $id = (int) escape($id); + $link = $LINKSDB[$id]; + $pluginManager->executeHooks('delete_link', $link); + unset($LINKSDB[$id]); + } $LINKSDB->save($conf->get('resource.page_cache')); // save to disk + $history->deleteLink($link); // If we are called from the bookmarklet, we must close the popup: if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo ''; exit; } @@@ -1373,7 -1384,7 +1373,7 @@@ 'link' => $link, 'link_is_new' => false, 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), - 'tags' => $LINKSDB->allTags(), + 'tags' => $LINKSDB->linksCountPerTag(), ); $pluginManager->executeHooks('render_editlink', $data); @@@ -1442,7 -1453,7 +1442,7 @@@ 'link_is_new' => $link_is_new, 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), - 'tags' => $LINKSDB->allTags(), + 'tags' => $LINKSDB->linksCountPerTag(), 'default_private_links' => $conf->get('privacy.default_private_links', false), ); $pluginManager->executeHooks('render_editlink', $data); @@@ -1504,22 -1515,7 +1504,22 @@@ if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { // Show import dialog - $PAGE->assign('maxfilesize', getMaxFileSize()); + $PAGE->assign( + 'maxfilesize', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + false + ) + ); + $PAGE->assign( + 'maxfilesizeHuman', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + true + ) + ); $PAGE->renderPage('import'); exit; } @@@ -1529,7 -1525,7 +1529,7 @@@ // The file is too big or some form field may be missing. echo ''; exit; @@@ -1541,8 -1537,7 +1541,8 @@@ $_POST, $_FILES, $LINKSDB, - $conf + $conf, + $history ); echo ''; @@@ -1582,7 -1577,6 +1582,7 @@@ $conf->set('general.enabled_plugins', save_plugin_config($_POST)); } $conf->write(isLoggedIn()); + $history->updateSettings(); } catch (Exception $e) { error_log( @@@ -1598,13 -1592,6 +1598,13 @@@ exit; } + // Get a fresh token + if ($targetPage == Router::$GET_TOKEN) { + header('Content-Type:text/plain'); + echo getToken($conf); + exit; + } + // -------- Otherwise, simply display search form and links: showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); exit; @@@ -1622,7 -1609,15 +1622,15 @@@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) { // Used in templates - $searchtags = !empty($_GET['searchtags']) ? escape(normalize_spaces($_GET['searchtags'])) : ''; + if (isset($_GET['searchtags'])) { + if (! empty($_GET['searchtags'])) { + $searchtags = escape(normalize_spaces($_GET['searchtags'])); + } else { + $searchtags = false; + } + } else { + $searchtags = ''; + } $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : ''; // Smallhash filter @@@ -1637,7 -1632,11 +1645,11 @@@ } else { // Filter links according search parameters. $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; - $linksToDisplay = $LINKSDB->filterSearch($_GET, false, $visibility); + $request = [ + 'searchtags' => $searchtags, + 'searchterm' => $searchterm, + ]; + $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility); } // ---- Handle paging. @@@ -1684,7 -1683,7 +1696,7 @@@ } // Compute paging navigation - $searchtagsUrl = empty($searchtags) ? '' : '&searchtags=' . urlencode($searchtags); + $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); $previous_page_url = ''; if ($i != count($keys)) { @@@ -1707,6 -1706,7 +1719,6 @@@ 'visibility' => ! empty($_SESSION['privateonly']) ? 'private' : '', 'redirector' => $conf->get('redirector.url'), // Optional redirector URL. 'links' => $linkDisp, - 'tags' => $LINKSDB->allTags(), ); // If there is only a single link, we change on-the-fly the title of the page. @@@ -1992,10 -1992,16 +2004,10 @@@ function install($conf exit; } - // Display config form: - list($timezone_form, $timezone_js) = generateTimeZoneForm(); - $timezone_html = ''; - if ($timezone_form != '') { - $timezone_html = 'Timezone:'.$timezone_form.''; - } - $PAGE = new PageBuilder($conf); - $PAGE->assign('timezone_html',$timezone_html); - $PAGE->assign('timezone_js',$timezone_js); + list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); + $PAGE->assign('continents', $continents); + $PAGE->assign('cities', $cities); $PAGE->renderPage('install'); exit; } @@@ -2239,27 -2245,16 +2251,27 @@@ $linkDb = new LinkDB $conf->get('redirector.encode_url') ); +try { + $history = new History($conf->get('resource.history')); +} catch(Exception $e) { + die($e->getMessage()); +} + $container = new \Slim\Container(); $container['conf'] = $conf; $container['plugins'] = $pluginManager; +$container['history'] = $history; $app = new \Slim\App($container); // REST API routes $app->group('/api/v1', function() { - $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo'); - $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks'); - $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink'); + $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo'); + $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks'); + $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink'); + $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); + $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); + $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); + $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory'); })->add('\Shaarli\Api\ApiMiddleware'); $response = $app->run(true); @@@ -2268,7 -2263,7 +2280,7 @@@ if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { // We use UTF-8 for proper international characters handling. header('Content-Type: text/html; charset=utf-8'); - renderPage($conf, $pluginManager, $linkDb); + renderPage($conf, $pluginManager, $linkDb, $history); } else { $app->respond($response); } diff --combined tests/LinkDBTest.php index 2523467d,6fbf597a..25438277 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php @@@ -101,7 -101,7 +101,7 @@@ class LinkDBTest extends PHPUnit_Framew * Attempt to instantiate a LinkDB whereas the datastore is not writable * * @expectedException IOException - * @expectedExceptionMessageRegExp /Error accessing\nnull/ + * @expectedExceptionMessageRegExp /Error accessing "null"/ */ public function testConstructDatastoreNotWriteable() { @@@ -297,7 -297,7 +297,7 @@@ 'sTuff' => 2, 'ut' => 1, ), - self::$publicLinkDB->allTags() + self::$publicLinkDB->linksCountPerTag() ); $this->assertEquals( @@@ -325,34 -325,7 +325,34 @@@ 'tag4' => 1, 'ut' => 1, ), - self::$privateLinkDB->allTags() + self::$privateLinkDB->linksCountPerTag() + ); + $this->assertEquals( + array( + 'web' => 4, + 'cartoon' => 2, + 'gnu' => 1, + 'dev' => 1, + 'samba' => 1, + 'media' => 1, + 'html' => 1, + 'w3c' => 1, + 'css' => 1, + 'Mercurial' => 1, + '.hidden' => 1, + 'hashtag' => 1, + ), + self::$privateLinkDB->linksCountPerTag(['web']) + ); + $this->assertEquals( + array( + 'web' => 1, + 'html' => 1, + 'w3c' => 1, + 'css' => 1, + 'Mercurial' => 1, + ), + self::$privateLinkDB->linksCountPerTag(['web'], 'private') ); } @@@ -475,7 -448,7 +475,7 @@@ public function testReorderLinksDesc() { self::$privateLinkDB->reorder('ASC'); - $linkIds = array(42, 4, 1, 0, 7, 6, 8, 41); + $linkIds = array(42, 4, 9, 1, 0, 7, 6, 8, 41); $cpt = 0; foreach (self::$privateLinkDB as $key => $value) { $this->assertEquals($linkIds[$cpt++], $key); diff --combined tests/api/controllers/GetLinksTest.php index 84ae7f7a,f1b262bc..4cb70224 --- a/tests/api/controllers/GetLinksTest.php +++ b/tests/api/controllers/GetLinksTest.php @@@ -61,7 -61,6 +61,7 @@@ class GetLinksTest extends \PHPUnit_Fra $this->container = new Container(); $this->container['conf'] = $this->conf; $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); + $this->container['history'] = null; $this->controller = new Links($this->container); } @@@ -95,7 -94,7 +95,7 @@@ $this->assertEquals($this->refDB->countLinks(), count($data)); // Check order - $order = [41, 8, 6, 7, 0, 1, 4, 42]; + $order = [41, 8, 6, 7, 0, 1, 9, 4, 42]; $cpt = 0; foreach ($data as $link) { $this->assertEquals(self::NB_FIELDS_LINK, count($link)); @@@ -164,7 -163,7 +164,7 @@@ $data = json_decode((string) $response->getBody(), true); $this->assertEquals($this->refDB->countLinks(), count($data)); // Check order - $order = [41, 8, 6, 7, 0, 1, 4, 42]; + $order = [41, 8, 6, 7, 0, 1, 9, 4, 42]; $cpt = 0; foreach ($data as $link) { $this->assertEquals(self::NB_FIELDS_LINK, count($link)); diff --combined tests/api/controllers/InfoTest.php index e85eb281,5d6a2329..f7e63bfa --- a/tests/api/controllers/InfoTest.php +++ b/tests/api/controllers/InfoTest.php @@@ -54,7 -54,6 +54,7 @@@ class InfoTest extends \PHPUnit_Framewo $this->container = new Container(); $this->container['conf'] = $this->conf; $this->container['db'] = new \LinkDB(self::$testDatastore, true, false); + $this->container['history'] = null; $this->controller = new Info($this->container); } @@@ -81,7 -80,7 +81,7 @@@ $this->assertEquals(200, $response->getStatusCode()); $data = json_decode((string) $response->getBody(), true); - $this->assertEquals(8, $data['global_counter']); + $this->assertEquals(\ReferenceLinkDB::$NB_LINKS_TOTAL, $data['global_counter']); $this->assertEquals(2, $data['private_counter']); $this->assertEquals('Shaarli', $data['settings']['title']); $this->assertEquals('?', $data['settings']['header_link']); @@@ -104,7 -103,7 +104,7 @@@ $this->assertEquals(200, $response->getStatusCode()); $data = json_decode((string) $response->getBody(), true); - $this->assertEquals(8, $data['global_counter']); + $this->assertEquals(\ReferenceLinkDB::$NB_LINKS_TOTAL, $data['global_counter']); $this->assertEquals(2, $data['private_counter']); $this->assertEquals($title, $data['settings']['title']); $this->assertEquals($headerLink, $data['settings']['header_link']); diff --combined tests/utils/ReferenceLinkDB.php index 1f4b3063,29d63fac..f09eebc1 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php @@@ -4,7 -4,7 +4,7 @@@ */ class ReferenceLinkDB { - public static $NB_LINKS_TOTAL = 8; + public static $NB_LINKS_TOTAL = 9; private $_links = array(); private $_publicCount = 0; @@@ -37,6 -37,16 +37,16 @@@ 'ut' ); + $this->addLink( + 9, + 'PSR-2: Coding Style Guide', + 'http://www.php-fig.org/psr/psr-2/', + 'This guide extends and expands on PSR-1, the basic coding standard.', + 0, + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_152312'), + '' + ); + $this->addLink( 8, 'Free as in Freedom 2.0 @website', @@@ -56,7 -66,7 +66,7 @@@ 0, DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'), 'gnu media web .hidden hashtag', - null, + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130615_184230'), 'IuWvgA' ); @@@ -161,6 -171,20 +171,20 @@@ return $this->_privateCount; } + /** + * Returns the number of links without tag + */ + public function countUntaggedLinks() + { + $cpt = 0; + foreach ($this->_links as $link) { + if (empty($link['tags'])) { + ++$cpt; + } + } + return $cpt; + } + public function getLinks() { return $this->_links; diff --combined tpl/default/linklist.html index 6a4e14a6,3d6be529..2568a5d6 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@@ -15,8 -15,6 +15,8 @@@ {/if} + + - {elseif="!empty($search_term) or !empty($search_tags) or !empty($visibility)"} + {elseif="!empty($search_term) or $search_tags !== '' or !empty($visibility)"}
@@@ -107,6 -105,10 +107,10 @@@ {$value} {/loop} + {elseif="$search_tags === false"} + + {'untagged'|t} + {/if} {if="!empty($visibility)"} {'with status'|t} @@@ -123,7 -125,7 +127,7 @@@
{loop="links"}
-