From d9ba1cdd44a7eec9e7f4d429087c6ba838ad473e Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 17 Jul 2018 14:13:37 +0200 Subject: Do not check the IP address with session protection disabled This allows the user to stay logged in if his IP changes. Fixes #1106 --- application/security/LoginManager.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'application') diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index d6784d6d..5a58926d 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -58,6 +58,9 @@ class LoginManager */ public function generateStaySignedInToken($clientIpAddress) { + if ($this->configManager->get('security.session_protection_disabled') === true) { + $clientIpAddress = ''; + } $this->staySignedInToken = sha1( $this->configManager->get('credentials.hash') . $clientIpAddress -- cgit v1.2.3 From bdc5152d486ca75372c271f94623b248bc127800 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Sun, 2 Dec 2018 23:24:58 +0100 Subject: namespacing: \Shaarli\History Signed-off-by: VirtualTam --- application/History.php | 13 +++++++++---- application/NetscapeBookmarkUtils.php | 1 + application/api/controllers/ApiController.php | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) (limited to 'application') diff --git a/application/History.php b/application/History.php index 35ec016a..8074a017 100644 --- a/application/History.php +++ b/application/History.php @@ -1,4 +1,9 @@ historyFilePath)) { + if (!is_file($this->historyFilePath)) { FileUtils::writeFlatDB($this->historyFilePath, []); } - if (! is_writable($this->historyFilePath)) { + if (!is_writable($this->historyFilePath)) { throw new Exception(t('History file isn\'t readable or writable')); } } @@ -191,7 +196,7 @@ class History */ protected function write() { - $comparaison = new DateTime('-'. $this->retentionTime . ' seconds'); + $comparaison = new DateTime('-' . $this->retentionTime . ' seconds'); foreach ($this->history as $key => $value) { if ($value['datetime'] < $comparaison) { unset($this->history[$key]); diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php index 84dd2b20..c0c007ea 100644 --- a/application/NetscapeBookmarkUtils.php +++ b/application/NetscapeBookmarkUtils.php @@ -2,6 +2,7 @@ use Psr\Log\LogLevel; use Shaarli\Config\ConfigManager; +use Shaarli\History; use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; use Katzgrau\KLogger\Logger; diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php index 9edefcf6..47e0e178 100644 --- a/application/api/controllers/ApiController.php +++ b/application/api/controllers/ApiController.php @@ -30,7 +30,7 @@ abstract class ApiController protected $linkDb; /** - * @var \History + * @var \Shaarli\History */ protected $history; -- cgit v1.2.3 From f3d2f257946e2a3c8791c1ba99b379acbe934fec Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Sun, 2 Dec 2018 23:31:40 +0100 Subject: namespacing: \Shaarli\Exceptions\IOException Signed-off-by: VirtualTam --- application/FileUtils.php | 2 ++ application/LinkDB.php | 3 +++ application/Updater.php | 1 + application/config/ConfigJson.php | 2 +- application/config/ConfigManager.php | 2 +- application/config/ConfigPhp.php | 2 +- application/exceptions/IOException.php | 5 ++++- 7 files changed, 13 insertions(+), 4 deletions(-) (limited to 'application') diff --git a/application/FileUtils.php b/application/FileUtils.php index b89ea12b..ba409821 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php @@ -1,5 +1,7 @@ path = $path; $this->message = empty($message) ? t('Error accessing') : $message; - $this->message .= ' "' . $this->path .'"'; + $this->message .= ' "' . $this->path . '"'; } } -- cgit v1.2.3 From dfc650aa239d3a2c028d0ba13132ce75b4f4c0b4 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Mon, 3 Dec 2018 00:08:04 +0100 Subject: namespacing: \Shaarli\Feed\{Cache,CachedPage,FeedBuilder} Signed-off-by: VirtualTam --- application/Cache.php | 38 ----- application/CachedPage.php | 59 -------- application/FeedBuilder.php | 296 -------------------------------------- application/feed/Cache.php | 38 +++++ application/feed/CachedPage.php | 61 ++++++++ application/feed/FeedBuilder.php | 299 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 398 insertions(+), 393 deletions(-) delete mode 100644 application/Cache.php delete mode 100644 application/CachedPage.php delete mode 100644 application/FeedBuilder.php create mode 100644 application/feed/Cache.php create mode 100644 application/feed/CachedPage.php create mode 100644 application/feed/FeedBuilder.php (limited to 'application') diff --git a/application/Cache.php b/application/Cache.php deleted file mode 100644 index e5d43e61..00000000 --- a/application/Cache.php +++ /dev/null @@ -1,38 +0,0 @@ -cacheDir = $cacheDir; - $this->filename = $this->cacheDir.'/'.sha1($url).'.cache'; - $this->shouldBeCached = $shouldBeCached; - } - - /** - * Returns the cached version of a page, if it exists and should be cached - * - * @return string a cached version of the page if it exists, null otherwise - */ - public function cachedVersion() - { - if (!$this->shouldBeCached) { - return null; - } - if (is_file($this->filename)) { - return file_get_contents($this->filename); - } - return null; - } - - /** - * Puts a page in the cache - * - * @param string $pageContent XML content to cache - */ - public function cache($pageContent) - { - if (!$this->shouldBeCached) { - return; - } - file_put_contents($this->filename, $pageContent); - } -} diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php deleted file mode 100644 index 73fafcbe..00000000 --- a/application/FeedBuilder.php +++ /dev/null @@ -1,296 +0,0 @@ -linkDB = $linkDB; - $this->feedType = $feedType; - $this->serverInfo = $serverInfo; - $this->userInput = $userInput; - $this->isLoggedIn = $isLoggedIn; - } - - /** - * Build data for feed templates. - * - * @return array Formatted data for feeds templates. - */ - public function buildData() - { - // Search for untagged links - if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { - $this->userInput['searchtags'] = false; - } - - // Optionally filter the results: - $linksToDisplay = $this->linkDB->filterSearch($this->userInput); - - $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); - - // Can't use array_keys() because $link is a LinkDB instance and not a real array. - $keys = array(); - foreach ($linksToDisplay as $key => $value) { - $keys[] = $key; - } - - $pageaddr = escape(index_url($this->serverInfo)); - $linkDisplayed = array(); - for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { - $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); - } - - $data['language'] = $this->getTypeLanguage(); - $data['last_update'] = $this->getLatestDateFormatted(); - $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; - // Remove leading slash from REQUEST_URI. - $data['self_link'] = escape(server_url($this->serverInfo)) - . escape($this->serverInfo['REQUEST_URI']); - $data['index_url'] = $pageaddr; - $data['usepermalinks'] = $this->usePermalinks === true; - $data['links'] = $linkDisplayed; - - return $data; - } - - /** - * Build a feed item (one per shaare). - * - * @param array $link Single link array extracted from LinkDB. - * @param string $pageaddr Index URL. - * - * @return array Link array with feed attributes. - */ - protected function buildItem($link, $pageaddr) - { - $link['guid'] = $pageaddr .'?'. $link['shorturl']; - // Check for both signs of a note: starting with ? and 7 chars long. - if ($link['url'][0] === '?' && strlen($link['url']) === 7) { - $link['url'] = $pageaddr . $link['url']; - } - if ($this->usePermalinks === true) { - $permalink = ''. t('Direct link') .''; - } else { - $permalink = ''. t('Permalink') .''; - } - $link['description'] = format_description($link['description'], '', false, $pageaddr); - $link['description'] .= PHP_EOL .'
— '. $permalink; - - $pubDate = $link['created']; - $link['pub_iso_date'] = $this->getIsoDate($pubDate); - - // atom:entry elements MUST contain exactly one atom:updated element. - if (!empty($link['updated'])) { - $upDate = $link['updated']; - $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM); - } else { - $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM); - ; - } - - // Save the more recent item. - if (empty($this->latestDate) || $this->latestDate < $pubDate) { - $this->latestDate = $pubDate; - } - if (!empty($upDate) && $this->latestDate < $upDate) { - $this->latestDate = $upDate; - } - - $taglist = array_filter(explode(' ', $link['tags']), 'strlen'); - uasort($taglist, 'strcasecmp'); - $link['taglist'] = $taglist; - - return $link; - } - - /** - * Set this to true to use permalinks instead of direct links. - * - * @param boolean $usePermalinks true to force permalinks. - */ - public function setUsePermalinks($usePermalinks) - { - $this->usePermalinks = $usePermalinks; - } - - /** - * Set this to true to hide timestamps in feeds. - * - * @param boolean $hideDates true to enable. - */ - public function setHideDates($hideDates) - { - $this->hideDates = $hideDates; - } - - /** - * Set the locale. Used to show feed language. - * - * @param string $locale The locale (eg. 'fr_FR.UTF8'). - */ - public function setLocale($locale) - { - $this->locale = strtolower($locale); - } - - /** - * Get the language according to the feed type, based on the locale: - * - * - RSS format: en-us (default: 'en-en'). - * - ATOM format: fr (default: 'en'). - * - * @return string The language. - */ - public function getTypeLanguage() - { - // Use the locale do define the language, if available. - if (! empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) { - $length = ($this->feedType == self::$FEED_RSS) ? 5 : 2; - return str_replace('_', '-', substr($this->locale, 0, $length)); - } - return ($this->feedType == self::$FEED_RSS) ? 'en-en' : 'en'; - } - - /** - * Format the latest item date found according to the feed type. - * - * Return an empty string if invalid DateTime is passed. - * - * @return string Formatted date. - */ - protected function getLatestDateFormatted() - { - if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) { - return ''; - } - - $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM; - return $this->latestDate->format($type); - } - - /** - * Get ISO date from DateTime according to feed type. - * - * @param DateTime $date Date to format. - * @param string|bool $format Force format. - * - * @return string Formatted date. - */ - protected function getIsoDate(DateTime $date, $format = false) - { - if ($format !== false) { - return $date->format($format); - } - if ($this->feedType == self::$FEED_RSS) { - return $date->format(DateTime::RSS); - } - return $date->format(DateTime::ATOM); - } - - /** - * Returns the number of link to display according to 'nb' user input parameter. - * - * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. - * If 'nb' is set to 'all', display all filtered links (max parameter). - * - * @param int $max maximum number of links to display. - * - * @return int number of links to display. - */ - public function getNbLinks($max) - { - if (empty($this->userInput['nb'])) { - return self::$DEFAULT_NB_LINKS; - } - - if ($this->userInput['nb'] == 'all') { - return $max; - } - - $intNb = intval($this->userInput['nb']); - if (! is_int($intNb) || $intNb == 0) { - return self::$DEFAULT_NB_LINKS; - } - - return $intNb; - } -} diff --git a/application/feed/Cache.php b/application/feed/Cache.php new file mode 100644 index 00000000..e5d43e61 --- /dev/null +++ b/application/feed/Cache.php @@ -0,0 +1,38 @@ +cacheDir = $cacheDir; + $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; + $this->shouldBeCached = $shouldBeCached; + } + + /** + * Returns the cached version of a page, if it exists and should be cached + * + * @return string a cached version of the page if it exists, null otherwise + */ + public function cachedVersion() + { + if (!$this->shouldBeCached) { + return null; + } + if (is_file($this->filename)) { + return file_get_contents($this->filename); + } + return null; + } + + /** + * Puts a page in the cache + * + * @param string $pageContent XML content to cache + */ + public function cache($pageContent) + { + if (!$this->shouldBeCached) { + return; + } + file_put_contents($this->filename, $pageContent); + } +} diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php new file mode 100644 index 00000000..dcfd2c89 --- /dev/null +++ b/application/feed/FeedBuilder.php @@ -0,0 +1,299 @@ +linkDB = $linkDB; + $this->feedType = $feedType; + $this->serverInfo = $serverInfo; + $this->userInput = $userInput; + $this->isLoggedIn = $isLoggedIn; + } + + /** + * Build data for feed templates. + * + * @return array Formatted data for feeds templates. + */ + public function buildData() + { + // Search for untagged links + if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) { + $this->userInput['searchtags'] = false; + } + + // Optionally filter the results: + $linksToDisplay = $this->linkDB->filterSearch($this->userInput); + + $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay)); + + // Can't use array_keys() because $link is a LinkDB instance and not a real array. + $keys = array(); + foreach ($linksToDisplay as $key => $value) { + $keys[] = $key; + } + + $pageaddr = escape(index_url($this->serverInfo)); + $linkDisplayed = array(); + for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { + $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr); + } + + $data['language'] = $this->getTypeLanguage(); + $data['last_update'] = $this->getLatestDateFormatted(); + $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; + // Remove leading slash from REQUEST_URI. + $data['self_link'] = escape(server_url($this->serverInfo)) + . escape($this->serverInfo['REQUEST_URI']); + $data['index_url'] = $pageaddr; + $data['usepermalinks'] = $this->usePermalinks === true; + $data['links'] = $linkDisplayed; + + return $data; + } + + /** + * Build a feed item (one per shaare). + * + * @param array $link Single link array extracted from LinkDB. + * @param string $pageaddr Index URL. + * + * @return array Link array with feed attributes. + */ + protected function buildItem($link, $pageaddr) + { + $link['guid'] = $pageaddr . '?' . $link['shorturl']; + // Check for both signs of a note: starting with ? and 7 chars long. + if ($link['url'][0] === '?' && strlen($link['url']) === 7) { + $link['url'] = $pageaddr . $link['url']; + } + if ($this->usePermalinks === true) { + $permalink = '' . t('Direct link') . ''; + } else { + $permalink = '' . t('Permalink') . ''; + } + $link['description'] = format_description($link['description'], '', false, $pageaddr); + $link['description'] .= PHP_EOL . '
— ' . $permalink; + + $pubDate = $link['created']; + $link['pub_iso_date'] = $this->getIsoDate($pubDate); + + // atom:entry elements MUST contain exactly one atom:updated element. + if (!empty($link['updated'])) { + $upDate = $link['updated']; + $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM); + } else { + $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM); + } + + // Save the more recent item. + if (empty($this->latestDate) || $this->latestDate < $pubDate) { + $this->latestDate = $pubDate; + } + if (!empty($upDate) && $this->latestDate < $upDate) { + $this->latestDate = $upDate; + } + + $taglist = array_filter(explode(' ', $link['tags']), 'strlen'); + uasort($taglist, 'strcasecmp'); + $link['taglist'] = $taglist; + + return $link; + } + + /** + * Set this to true to use permalinks instead of direct links. + * + * @param boolean $usePermalinks true to force permalinks. + */ + public function setUsePermalinks($usePermalinks) + { + $this->usePermalinks = $usePermalinks; + } + + /** + * Set this to true to hide timestamps in feeds. + * + * @param boolean $hideDates true to enable. + */ + public function setHideDates($hideDates) + { + $this->hideDates = $hideDates; + } + + /** + * Set the locale. Used to show feed language. + * + * @param string $locale The locale (eg. 'fr_FR.UTF8'). + */ + public function setLocale($locale) + { + $this->locale = strtolower($locale); + } + + /** + * Get the language according to the feed type, based on the locale: + * + * - RSS format: en-us (default: 'en-en'). + * - ATOM format: fr (default: 'en'). + * + * @return string The language. + */ + public function getTypeLanguage() + { + // Use the locale do define the language, if available. + if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) { + $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2; + return str_replace('_', '-', substr($this->locale, 0, $length)); + } + return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en'; + } + + /** + * Format the latest item date found according to the feed type. + * + * Return an empty string if invalid DateTime is passed. + * + * @return string Formatted date. + */ + protected function getLatestDateFormatted() + { + if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) { + return ''; + } + + $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM; + return $this->latestDate->format($type); + } + + /** + * Get ISO date from DateTime according to feed type. + * + * @param DateTime $date Date to format. + * @param string|bool $format Force format. + * + * @return string Formatted date. + */ + protected function getIsoDate(DateTime $date, $format = false) + { + if ($format !== false) { + return $date->format($format); + } + if ($this->feedType == self::$FEED_RSS) { + return $date->format(DateTime::RSS); + } + return $date->format(DateTime::ATOM); + } + + /** + * Returns the number of link to display according to 'nb' user input parameter. + * + * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. + * If 'nb' is set to 'all', display all filtered links (max parameter). + * + * @param int $max maximum number of links to display. + * + * @return int number of links to display. + */ + public function getNbLinks($max) + { + if (empty($this->userInput['nb'])) { + return self::$DEFAULT_NB_LINKS; + } + + if ($this->userInput['nb'] == 'all') { + return $max; + } + + $intNb = intval($this->userInput['nb']); + if (!is_int($intNb) || $intNb == 0) { + return self::$DEFAULT_NB_LINKS; + } + + return $intNb; + } +} -- cgit v1.2.3 From 00af48d9d20af1ce51c8ad42fe354fafc9ceb8a3 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Mon, 3 Dec 2018 00:16:10 +0100 Subject: namespacing: \Shaarli\Http\Base64Url Signed-off-by: VirtualTam --- application/Base64Url.php | 35 ----------------------------------- application/api/ApiUtils.php | 6 +++--- application/http/Base64Url.php | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 38 deletions(-) delete mode 100644 application/Base64Url.php create mode 100644 application/http/Base64Url.php (limited to 'application') diff --git a/application/Base64Url.php b/application/Base64Url.php deleted file mode 100644 index 54d0fcd5..00000000 --- a/application/Base64Url.php +++ /dev/null @@ -1,35 +0,0 @@ - Date: Mon, 3 Dec 2018 00:23:35 +0100 Subject: namespacing: \Shaarli\Http\Url Signed-off-by: VirtualTam --- application/HttpUtils.php | 3 + application/Url.php | 218 +--------------------------------------------- application/http/Url.php | 217 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+), 216 deletions(-) create mode 100644 application/http/Url.php (limited to 'application') diff --git a/application/HttpUtils.php b/application/HttpUtils.php index 9c438160..51af5d0d 100644 --- a/application/HttpUtils.php +++ b/application/HttpUtils.php @@ -1,4 +1,7 @@ cleanup(); } @@ -47,7 +47,7 @@ function cleanup_url($url) */ function get_url_scheme($url) { - $obj_url = new Url($url); + $obj_url = new \Shaarli\Http\Url($url); return $obj_url->getScheme(); } @@ -86,217 +86,3 @@ function whitelist_protocols($url, $protocols) } return $url; } - -/** - * URL representation and cleanup utilities - * - * Form - * scheme://[username:password@]host[:port][/path][?query][#fragment] - * - * Examples - * http://username:password@hostname:9090/path?arg1=value1&arg2=value2#anchor - * https://host.name.tld - * https://h2.g2/faq/?vendor=hitchhiker&item=guide&dest=galaxy#answer - * - * @see http://www.faqs.org/rfcs/rfc3986.html - */ -class Url -{ - private static $annoyingQueryParams = array( - // Facebook - 'action_object_map=', - 'action_ref_map=', - 'action_type_map=', - 'fb_', - 'fb=', - 'PHPSESSID=', - - // Scoop.it - '__scoop', - - // Google Analytics & FeedProxy - 'utm_', - - // ATInternet - 'xtor=', - - // Other - 'campaign_' - ); - - private static $annoyingFragments = array( - // ATInternet - 'xtor=RSS-', - - // Misc. - 'tk.rss_all' - ); - - /* - * URL parts represented as an array - * - * @see http://php.net/parse_url - */ - protected $parts; - - /** - * Parses a string containing a URL - * - * @param string $url a string containing a URL - */ - public function __construct($url) - { - $url = self::cleanupUnparsedUrl(trim($url)); - $this->parts = parse_url($url); - - if (!empty($url) && empty($this->parts['scheme'])) { - $this->parts['scheme'] = 'http'; - } - } - - /** - * Clean up URL before it's parsed. - * ie. handle urlencode, url prefixes, etc. - * - * @param string $url URL to clean. - * - * @return string cleaned URL. - */ - protected static function cleanupUnparsedUrl($url) - { - return self::removeFirefoxAboutReader($url); - } - - /** - * Remove Firefox Reader prefix if it's present. - * - * @param string $input url - * - * @return string cleaned url - */ - protected static function removeFirefoxAboutReader($input) - { - $firefoxPrefix = 'about://reader?url='; - if (startsWith($input, $firefoxPrefix)) { - return urldecode(ltrim($input, $firefoxPrefix)); - } - return $input; - } - - /** - * Returns a string representation of this URL - */ - public function toString() - { - return unparse_url($this->parts); - } - - /** - * Removes undesired query parameters - */ - protected function cleanupQuery() - { - if (! isset($this->parts['query'])) { - return; - } - - $queryParams = explode('&', $this->parts['query']); - - foreach (self::$annoyingQueryParams as $annoying) { - foreach ($queryParams as $param) { - if (startsWith($param, $annoying)) { - $queryParams = array_diff($queryParams, array($param)); - continue; - } - } - } - - if (count($queryParams) == 0) { - unset($this->parts['query']); - return; - } - - $this->parts['query'] = implode('&', $queryParams); - } - - /** - * Removes undesired fragments - */ - protected function cleanupFragment() - { - if (! isset($this->parts['fragment'])) { - return; - } - - foreach (self::$annoyingFragments as $annoying) { - if (startsWith($this->parts['fragment'], $annoying)) { - unset($this->parts['fragment']); - break; - } - } - } - - /** - * Removes undesired query parameters and fragments - * - * @return string the string representation of this URL after cleanup - */ - public function cleanup() - { - $this->cleanupQuery(); - $this->cleanupFragment(); - return $this->toString(); - } - - /** - * Converts an URL with an International Domain Name host to a ASCII one. - * This requires PHP-intl. If it's not available, just returns this->cleanup(). - * - * @return string converted cleaned up URL. - */ - public function idnToAscii() - { - $out = $this->cleanup(); - if (! function_exists('idn_to_ascii') || ! isset($this->parts['host'])) { - return $out; - } - $asciiHost = idn_to_ascii($this->parts['host'], 0, INTL_IDNA_VARIANT_UTS46); - return str_replace($this->parts['host'], $asciiHost, $out); - } - - /** - * Get URL scheme. - * - * @return string the URL scheme or false if none is provided. - */ - public function getScheme() - { - if (!isset($this->parts['scheme'])) { - return false; - } - return $this->parts['scheme']; - } - - /** - * Get URL host. - * - * @return string the URL host or false if none is provided. - */ - public function getHost() - { - if (empty($this->parts['host'])) { - return false; - } - return $this->parts['host']; - } - - /** - * 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; - } -} diff --git a/application/http/Url.php b/application/http/Url.php new file mode 100644 index 00000000..260231c6 --- /dev/null +++ b/application/http/Url.php @@ -0,0 +1,217 @@ +parts = parse_url($url); + + if (!empty($url) && empty($this->parts['scheme'])) { + $this->parts['scheme'] = 'http'; + } + } + + /** + * Clean up URL before it's parsed. + * ie. handle urlencode, url prefixes, etc. + * + * @param string $url URL to clean. + * + * @return string cleaned URL. + */ + protected static function cleanupUnparsedUrl($url) + { + return self::removeFirefoxAboutReader($url); + } + + /** + * Remove Firefox Reader prefix if it's present. + * + * @param string $input url + * + * @return string cleaned url + */ + protected static function removeFirefoxAboutReader($input) + { + $firefoxPrefix = 'about://reader?url='; + if (startsWith($input, $firefoxPrefix)) { + return urldecode(ltrim($input, $firefoxPrefix)); + } + return $input; + } + + /** + * Returns a string representation of this URL + */ + public function toString() + { + return unparse_url($this->parts); + } + + /** + * Removes undesired query parameters + */ + protected function cleanupQuery() + { + if (!isset($this->parts['query'])) { + return; + } + + $queryParams = explode('&', $this->parts['query']); + + foreach (self::$annoyingQueryParams as $annoying) { + foreach ($queryParams as $param) { + if (startsWith($param, $annoying)) { + $queryParams = array_diff($queryParams, array($param)); + continue; + } + } + } + + if (count($queryParams) == 0) { + unset($this->parts['query']); + return; + } + + $this->parts['query'] = implode('&', $queryParams); + } + + /** + * Removes undesired fragments + */ + protected function cleanupFragment() + { + if (!isset($this->parts['fragment'])) { + return; + } + + foreach (self::$annoyingFragments as $annoying) { + if (startsWith($this->parts['fragment'], $annoying)) { + unset($this->parts['fragment']); + break; + } + } + } + + /** + * Removes undesired query parameters and fragments + * + * @return string the string representation of this URL after cleanup + */ + public function cleanup() + { + $this->cleanupQuery(); + $this->cleanupFragment(); + return $this->toString(); + } + + /** + * Converts an URL with an International Domain Name host to a ASCII one. + * This requires PHP-intl. If it's not available, just returns this->cleanup(). + * + * @return string converted cleaned up URL. + */ + public function idnToAscii() + { + $out = $this->cleanup(); + if (!function_exists('idn_to_ascii') || !isset($this->parts['host'])) { + return $out; + } + $asciiHost = idn_to_ascii($this->parts['host'], 0, INTL_IDNA_VARIANT_UTS46); + return str_replace($this->parts['host'], $asciiHost, $out); + } + + /** + * Get URL scheme. + * + * @return string the URL scheme or false if none is provided. + */ + public function getScheme() + { + if (!isset($this->parts['scheme'])) { + return false; + } + return $this->parts['scheme']; + } + + /** + * Get URL host. + * + * @return string the URL host or false if none is provided. + */ + public function getHost() + { + if (empty($this->parts['host'])) { + return false; + } + return $this->parts['host']; + } + + /** + * 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; + } +} -- cgit v1.2.3 From 51753e403fa69c0ce124ede27d300477e3e799ca Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Mon, 3 Dec 2018 00:34:53 +0100 Subject: namespacing: move HTTP utilities along \Shaarli\Http\ classes Signed-off-by: VirtualTam --- application/HttpUtils.php | 479 ----------------------------------------- application/Url.php | 88 -------- application/http/HttpUtils.php | 479 +++++++++++++++++++++++++++++++++++++++++ application/http/Url.php | 2 +- application/http/UrlUtils.php | 88 ++++++++ 5 files changed, 568 insertions(+), 568 deletions(-) delete mode 100644 application/HttpUtils.php delete mode 100644 application/Url.php create mode 100644 application/http/HttpUtils.php create mode 100644 application/http/UrlUtils.php (limited to 'application') diff --git a/application/HttpUtils.php b/application/HttpUtils.php deleted file mode 100644 index 51af5d0d..00000000 --- a/application/HttpUtils.php +++ /dev/null @@ -1,479 +0,0 @@ -idnToAscii(); - - if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { - return array(array(0 => 'Invalid HTTP Url'), false); - } - - $userAgent = - 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)' - . ' Gecko/20100101 Firefox/45.0'; - $acceptLanguage = - substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3'; - $maxRedirs = 3; - - if (!function_exists('curl_init')) { - return get_http_response_fallback( - $cleanUrl, - $timeout, - $maxBytes, - $userAgent, - $acceptLanguage, - $maxRedirs - ); - } - - $ch = curl_init($cleanUrl); - if ($ch === false) { - return array(array(0 => 'curl_init() error'), false); - } - - // General cURL settings - curl_setopt($ch, CURLOPT_AUTOREFERER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_HEADER, true); - curl_setopt( - $ch, - CURLOPT_HTTPHEADER, - array('Accept-Language: ' . $acceptLanguage) - ); - curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); - - if (is_callable($curlWriteFunction)) { - curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction); - } - - // Max download size management - curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16); - curl_setopt($ch, CURLOPT_NOPROGRESS, false); - curl_setopt( - $ch, - CURLOPT_PROGRESSFUNCTION, - function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) { - if (version_compare(phpversion(), '5.5', '<')) { - // PHP version lower than 5.5 - // Callback has 4 arguments - $downloaded = $arg1; - } else { - // Callback has 5 arguments - $downloaded = $arg2; - } - // Non-zero return stops downloading - return ($downloaded > $maxBytes) ? 1 : 0; - } - ); - - $response = curl_exec($ch); - $errorNo = curl_errno($ch); - $errorStr = curl_error($ch); - $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - curl_close($ch); - - if ($response === false) { - if ($errorNo == CURLE_COULDNT_RESOLVE_HOST) { - /* - * Workaround to match fallback method behaviour - * Removing this would require updating - * GetHttpUrlTest::testGetInvalidRemoteUrl() - */ - return array(false, false); - } - return array(array(0 => 'curl_exec() error: ' . $errorStr), false); - } - - // Formatting output like the fallback method - $rawHeaders = substr($response, 0, $headSize); - - // Keep only headers from latest redirection - $rawHeadersArrayRedirs = explode("\r\n\r\n", trim($rawHeaders)); - $rawHeadersLastRedir = end($rawHeadersArrayRedirs); - - $content = substr($response, $headSize); - $headers = array(); - foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) { - if (empty($line) || ctype_space($line)) { - continue; - } - $splitLine = explode(': ', $line, 2); - if (count($splitLine) > 1) { - $key = $splitLine[0]; - $value = $splitLine[1]; - if (array_key_exists($key, $headers)) { - if (!is_array($headers[$key])) { - $headers[$key] = array(0 => $headers[$key]); - } - $headers[$key][] = $value; - } else { - $headers[$key] = $value; - } - } else { - $headers[] = $splitLine[0]; - } - } - - return array($headers, $content); -} - -/** - * GET an HTTP URL to retrieve its content (fallback method) - * - * @param string $cleanUrl URL to get (http://... valid and in ASCII form) - * @param int $timeout network timeout (in seconds) - * @param int $maxBytes maximum downloaded bytes - * @param string $userAgent "User-Agent" header - * @param string $acceptLanguage "Accept-Language" header - * @param int $maxRedr maximum amount of redirections followed - * - * @return array HTTP response headers, downloaded content - * - * Output format: - * [0] = associative array containing HTTP response headers - * [1] = URL content (downloaded data) - * - * @see http://php.net/manual/en/function.file-get-contents.php - * @see http://php.net/manual/en/function.stream-context-create.php - * @see http://php.net/manual/en/function.get-headers.php - */ -function get_http_response_fallback( - $cleanUrl, - $timeout, - $maxBytes, - $userAgent, - $acceptLanguage, - $maxRedr -) { - $options = array( - 'http' => array( - 'method' => 'GET', - 'timeout' => $timeout, - 'user_agent' => $userAgent, - 'header' => "Accept: */*\r\n" - . 'Accept-Language: ' . $acceptLanguage - ) - ); - - stream_context_set_default($options); - list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); - if (! $headers || strpos($headers[0], '200 OK') === false) { - $options['http']['request_fulluri'] = true; - stream_context_set_default($options); - list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); - } - - if (! $headers) { - return array($headers, false); - } - - try { - // TODO: catch Exception in calling code (thumbnailer) - $context = stream_context_create($options); - $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes); - } catch (Exception $exc) { - return array(array(0 => 'HTTP Error'), $exc->getMessage()); - } - - return array($headers, $content); -} - -/** - * Retrieve HTTP headers, following n redirections (temporary and permanent ones). - * - * @param string $url initial URL to reach. - * @param int $redirectionLimit max redirection follow. - * - * @return array HTTP headers, or false if it failed. - */ -function get_redirected_headers($url, $redirectionLimit = 3) -{ - $headers = get_headers($url, 1); - if (!empty($headers['location']) && empty($headers['Location'])) { - $headers['Location'] = $headers['location']; - } - - // 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) { - $redirection = getAbsoluteUrl($url, $redirection); - return get_redirected_headers($redirection, $redirectionLimit); - } - } - - return array($headers, $url); -} - -/** - * Get an absolute URL from a complete one, and another absolute/relative URL. - * - * @param string $originalUrl The original complete URL. - * @param string $newUrl The new one, absolute or relative. - * - * @return string Final URL: - * - $newUrl if it was already an absolute URL. - * - if it was relative, absolute URL from $originalUrl path. - */ -function getAbsoluteUrl($originalUrl, $newUrl) -{ - $newScheme = parse_url($newUrl, PHP_URL_SCHEME); - // Already an absolute URL. - if (!empty($newScheme)) { - return $newUrl; - } - - $parts = parse_url($originalUrl); - $final = $parts['scheme'] .'://'. $parts['host']; - $final .= (!empty($parts['port'])) ? $parts['port'] : ''; - $final .= '/'; - if ($newUrl[0] != '/') { - $final .= substr(ltrim($parts['path'], '/'), 0, strrpos($parts['path'], '/')); - } - $final .= ltrim($newUrl, '/'); - return $final; -} - -/** - * Returns the server's base URL: scheme://domain.tld[:port] - * - * @param array $server the $_SERVER array - * - * @return string the server's base URL - * - * @see http://www.ietf.org/rfc/rfc7239.txt - * @see http://www.ietf.org/rfc/rfc6648.txt - * @see http://stackoverflow.com/a/3561399 - * @see http://stackoverflow.com/q/452375 - */ -function server_url($server) -{ - $scheme = 'http'; - $port = ''; - - // Shaarli is served behind a proxy - if (isset($server['HTTP_X_FORWARDED_PROTO'])) { - // Keep forwarded scheme - if (strpos($server['HTTP_X_FORWARDED_PROTO'], ',') !== false) { - $schemes = explode(',', $server['HTTP_X_FORWARDED_PROTO']); - $scheme = trim($schemes[0]); - } else { - $scheme = $server['HTTP_X_FORWARDED_PROTO']; - } - - if (isset($server['HTTP_X_FORWARDED_PORT'])) { - // Keep forwarded port - if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) { - $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']); - $port = trim($ports[0]); - } else { - $port = $server['HTTP_X_FORWARDED_PORT']; - } - - // This is a workaround for proxies that don't forward the scheme properly. - // Connecting over port 443 has to be in HTTPS. - // See https://github.com/shaarli/Shaarli/issues/1022 - if ($port == '443') { - $scheme = 'https'; - } - - if (($scheme == 'http' && $port != '80') - || ($scheme == 'https' && $port != '443') - ) { - $port = ':' . $port; - } else { - $port = ''; - } - } - - if (isset($server['HTTP_X_FORWARDED_HOST'])) { - // Keep forwarded host - if (strpos($server['HTTP_X_FORWARDED_HOST'], ',') !== false) { - $hosts = explode(',', $server['HTTP_X_FORWARDED_HOST']); - $host = trim($hosts[0]); - } else { - $host = $server['HTTP_X_FORWARDED_HOST']; - } - } else { - $host = $server['SERVER_NAME']; - } - - return $scheme.'://'.$host.$port; - } - - // SSL detection - if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') - || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) { - $scheme = 'https'; - } - - // Do not append standard port values - if (($scheme == 'http' && $server['SERVER_PORT'] != '80') - || ($scheme == 'https' && $server['SERVER_PORT'] != '443')) { - $port = ':'.$server['SERVER_PORT']; - } - - return $scheme.'://'.$server['SERVER_NAME'].$port; -} - -/** - * Returns the absolute URL of the current script, without the query - * - * If the resource is "index.php", then it is removed (for better-looking URLs) - * - * @param array $server the $_SERVER array - * - * @return string the absolute URL of the current script, without the query - */ -function index_url($server) -{ - $scriptname = $server['SCRIPT_NAME']; - if (endsWith($scriptname, 'index.php')) { - $scriptname = substr($scriptname, 0, -9); - } - return server_url($server) . $scriptname; -} - -/** - * Returns the absolute URL of the current script, with the query - * - * If the resource is "index.php", then it is removed (for better-looking URLs) - * - * @param array $server the $_SERVER array - * - * @return string the absolute URL of the current script, with the query - */ -function page_url($server) -{ - if (! empty($server['QUERY_STRING'])) { - return index_url($server).'?'.$server['QUERY_STRING']; - } - return index_url($server); -} - -/** - * Retrieve the initial IP forwarded by the reverse proxy. - * - * Inspired from: https://github.com/zendframework/zend-http/blob/master/src/PhpEnvironment/RemoteAddress.php - * - * @param array $server $_SERVER array which contains HTTP headers. - * @param array $trustedIps List of trusted IP from the configuration. - * - * @return string|bool The forwarded IP, or false if none could be extracted. - */ -function getIpAddressFromProxy($server, $trustedIps) -{ - $forwardedIpHeader = 'HTTP_X_FORWARDED_FOR'; - if (empty($server[$forwardedIpHeader])) { - return false; - } - - $ips = preg_split('/\s*,\s*/', $server[$forwardedIpHeader]); - $ips = array_diff($ips, $trustedIps); - if (empty($ips)) { - return false; - } - - return array_pop($ips); -} - - -/** - * Return an identifier based on the advertised client IP address(es) - * - * This aims at preventing session hijacking from users behind the same proxy - * by relying on HTTP headers. - * - * See: - * - https://secure.php.net/manual/en/reserved.variables.server.php - * - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php - * - https://stackoverflow.com/questions/12233406/preventing-session-hijacking - * - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor - * - * @param array $server The $_SERVER array - * - * @return string An identifier based on client IP address information - */ -function client_ip_id($server) -{ - $ip = $server['REMOTE_ADDR']; - - if (isset($server['HTTP_X_FORWARDED_FOR'])) { - $ip = $ip . '_' . $server['HTTP_X_FORWARDED_FOR']; - } - if (isset($server['HTTP_CLIENT_IP'])) { - $ip = $ip . '_' . $server['HTTP_CLIENT_IP']; - } - return $ip; -} - - -/** - * Returns true if Shaarli's currently browsed in HTTPS. - * Supports reverse proxies (if the headers are correctly set). - * - * @param array $server $_SERVER. - * - * @return bool true if HTTPS, false otherwise. - */ -function is_https($server) -{ - - if (isset($server['HTTP_X_FORWARDED_PORT'])) { - // Keep forwarded port - if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) { - $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']); - $port = trim($ports[0]); - } else { - $port = $server['HTTP_X_FORWARDED_PORT']; - } - - if ($port == '443') { - return true; - } - } - - return ! empty($server['HTTPS']); -} diff --git a/application/Url.php b/application/Url.php deleted file mode 100644 index 81f72fb0..00000000 --- a/application/Url.php +++ /dev/null @@ -1,88 +0,0 @@ -cleanup(); -} - -/** - * Get URL scheme. - * - * @param string url Url for which the scheme is requested - * - * @return mixed the URL scheme or false if none is provided. - */ -function get_url_scheme($url) -{ - $obj_url = new \Shaarli\Http\Url($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, '/') ? '/' : ''); -} - -/** - * Replace not whitelisted protocols by 'http://' from given URL. - * - * @param string $url URL to clean - * @param array $protocols List of allowed protocols (aside from http(s)). - * - * @return string URL with allowed protocol - */ -function whitelist_protocols($url, $protocols) -{ - if (startsWith($url, '?') || startsWith($url, '/')) { - return $url; - } - $protocols = array_merge(['http', 'https'], $protocols); - $protocol = preg_match('#^(\w+):/?/?#', $url, $match); - // Protocol not allowed: we remove it and replace it with http - if ($protocol === 1 && ! in_array($match[1], $protocols)) { - $url = str_replace($match[0], 'http://', $url); - } elseif ($protocol !== 1) { - $url = 'http://' . $url; - } - return $url; -} diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php new file mode 100644 index 00000000..2ea9195d --- /dev/null +++ b/application/http/HttpUtils.php @@ -0,0 +1,479 @@ +idnToAscii(); + + if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { + return array(array(0 => 'Invalid HTTP UrlUtils'), false); + } + + $userAgent = + 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)' + . ' Gecko/20100101 Firefox/45.0'; + $acceptLanguage = + substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3'; + $maxRedirs = 3; + + if (!function_exists('curl_init')) { + return get_http_response_fallback( + $cleanUrl, + $timeout, + $maxBytes, + $userAgent, + $acceptLanguage, + $maxRedirs + ); + } + + $ch = curl_init($cleanUrl); + if ($ch === false) { + return array(array(0 => 'curl_init() error'), false); + } + + // General cURL settings + curl_setopt($ch, CURLOPT_AUTOREFERER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt( + $ch, + CURLOPT_HTTPHEADER, + array('Accept-Language: ' . $acceptLanguage) + ); + curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); + + if (is_callable($curlWriteFunction)) { + curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction); + } + + // Max download size management + curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16); + curl_setopt($ch, CURLOPT_NOPROGRESS, false); + curl_setopt( + $ch, + CURLOPT_PROGRESSFUNCTION, + function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) { + if (version_compare(phpversion(), '5.5', '<')) { + // PHP version lower than 5.5 + // Callback has 4 arguments + $downloaded = $arg1; + } else { + // Callback has 5 arguments + $downloaded = $arg2; + } + // Non-zero return stops downloading + return ($downloaded > $maxBytes) ? 1 : 0; + } + ); + + $response = curl_exec($ch); + $errorNo = curl_errno($ch); + $errorStr = curl_error($ch); + $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + curl_close($ch); + + if ($response === false) { + if ($errorNo == CURLE_COULDNT_RESOLVE_HOST) { + /* + * Workaround to match fallback method behaviour + * Removing this would require updating + * GetHttpUrlTest::testGetInvalidRemoteUrl() + */ + return array(false, false); + } + return array(array(0 => 'curl_exec() error: ' . $errorStr), false); + } + + // Formatting output like the fallback method + $rawHeaders = substr($response, 0, $headSize); + + // Keep only headers from latest redirection + $rawHeadersArrayRedirs = explode("\r\n\r\n", trim($rawHeaders)); + $rawHeadersLastRedir = end($rawHeadersArrayRedirs); + + $content = substr($response, $headSize); + $headers = array(); + foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) { + if (empty($line) || ctype_space($line)) { + continue; + } + $splitLine = explode(': ', $line, 2); + if (count($splitLine) > 1) { + $key = $splitLine[0]; + $value = $splitLine[1]; + if (array_key_exists($key, $headers)) { + if (!is_array($headers[$key])) { + $headers[$key] = array(0 => $headers[$key]); + } + $headers[$key][] = $value; + } else { + $headers[$key] = $value; + } + } else { + $headers[] = $splitLine[0]; + } + } + + return array($headers, $content); +} + +/** + * GET an HTTP URL to retrieve its content (fallback method) + * + * @param string $cleanUrl URL to get (http://... valid and in ASCII form) + * @param int $timeout network timeout (in seconds) + * @param int $maxBytes maximum downloaded bytes + * @param string $userAgent "User-Agent" header + * @param string $acceptLanguage "Accept-Language" header + * @param int $maxRedr maximum amount of redirections followed + * + * @return array HTTP response headers, downloaded content + * + * Output format: + * [0] = associative array containing HTTP response headers + * [1] = URL content (downloaded data) + * + * @see http://php.net/manual/en/function.file-get-contents.php + * @see http://php.net/manual/en/function.stream-context-create.php + * @see http://php.net/manual/en/function.get-headers.php + */ +function get_http_response_fallback( + $cleanUrl, + $timeout, + $maxBytes, + $userAgent, + $acceptLanguage, + $maxRedr +) { + $options = array( + 'http' => array( + 'method' => 'GET', + 'timeout' => $timeout, + 'user_agent' => $userAgent, + 'header' => "Accept: */*\r\n" + . 'Accept-Language: ' . $acceptLanguage + ) + ); + + stream_context_set_default($options); + list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); + if (! $headers || strpos($headers[0], '200 OK') === false) { + $options['http']['request_fulluri'] = true; + stream_context_set_default($options); + list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr); + } + + if (! $headers) { + return array($headers, false); + } + + try { + // TODO: catch Exception in calling code (thumbnailer) + $context = stream_context_create($options); + $content = file_get_contents($finalUrl, false, $context, -1, $maxBytes); + } catch (Exception $exc) { + return array(array(0 => 'HTTP Error'), $exc->getMessage()); + } + + return array($headers, $content); +} + +/** + * Retrieve HTTP headers, following n redirections (temporary and permanent ones). + * + * @param string $url initial URL to reach. + * @param int $redirectionLimit max redirection follow. + * + * @return array HTTP headers, or false if it failed. + */ +function get_redirected_headers($url, $redirectionLimit = 3) +{ + $headers = get_headers($url, 1); + if (!empty($headers['location']) && empty($headers['Location'])) { + $headers['Location'] = $headers['location']; + } + + // 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) { + $redirection = getAbsoluteUrl($url, $redirection); + return get_redirected_headers($redirection, $redirectionLimit); + } + } + + return array($headers, $url); +} + +/** + * Get an absolute URL from a complete one, and another absolute/relative URL. + * + * @param string $originalUrl The original complete URL. + * @param string $newUrl The new one, absolute or relative. + * + * @return string Final URL: + * - $newUrl if it was already an absolute URL. + * - if it was relative, absolute URL from $originalUrl path. + */ +function getAbsoluteUrl($originalUrl, $newUrl) +{ + $newScheme = parse_url($newUrl, PHP_URL_SCHEME); + // Already an absolute URL. + if (!empty($newScheme)) { + return $newUrl; + } + + $parts = parse_url($originalUrl); + $final = $parts['scheme'] .'://'. $parts['host']; + $final .= (!empty($parts['port'])) ? $parts['port'] : ''; + $final .= '/'; + if ($newUrl[0] != '/') { + $final .= substr(ltrim($parts['path'], '/'), 0, strrpos($parts['path'], '/')); + } + $final .= ltrim($newUrl, '/'); + return $final; +} + +/** + * Returns the server's base URL: scheme://domain.tld[:port] + * + * @param array $server the $_SERVER array + * + * @return string the server's base URL + * + * @see http://www.ietf.org/rfc/rfc7239.txt + * @see http://www.ietf.org/rfc/rfc6648.txt + * @see http://stackoverflow.com/a/3561399 + * @see http://stackoverflow.com/q/452375 + */ +function server_url($server) +{ + $scheme = 'http'; + $port = ''; + + // Shaarli is served behind a proxy + if (isset($server['HTTP_X_FORWARDED_PROTO'])) { + // Keep forwarded scheme + if (strpos($server['HTTP_X_FORWARDED_PROTO'], ',') !== false) { + $schemes = explode(',', $server['HTTP_X_FORWARDED_PROTO']); + $scheme = trim($schemes[0]); + } else { + $scheme = $server['HTTP_X_FORWARDED_PROTO']; + } + + if (isset($server['HTTP_X_FORWARDED_PORT'])) { + // Keep forwarded port + if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) { + $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']); + $port = trim($ports[0]); + } else { + $port = $server['HTTP_X_FORWARDED_PORT']; + } + + // This is a workaround for proxies that don't forward the scheme properly. + // Connecting over port 443 has to be in HTTPS. + // See https://github.com/shaarli/Shaarli/issues/1022 + if ($port == '443') { + $scheme = 'https'; + } + + if (($scheme == 'http' && $port != '80') + || ($scheme == 'https' && $port != '443') + ) { + $port = ':' . $port; + } else { + $port = ''; + } + } + + if (isset($server['HTTP_X_FORWARDED_HOST'])) { + // Keep forwarded host + if (strpos($server['HTTP_X_FORWARDED_HOST'], ',') !== false) { + $hosts = explode(',', $server['HTTP_X_FORWARDED_HOST']); + $host = trim($hosts[0]); + } else { + $host = $server['HTTP_X_FORWARDED_HOST']; + } + } else { + $host = $server['SERVER_NAME']; + } + + return $scheme.'://'.$host.$port; + } + + // SSL detection + if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on') + || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) { + $scheme = 'https'; + } + + // Do not append standard port values + if (($scheme == 'http' && $server['SERVER_PORT'] != '80') + || ($scheme == 'https' && $server['SERVER_PORT'] != '443')) { + $port = ':'.$server['SERVER_PORT']; + } + + return $scheme.'://'.$server['SERVER_NAME'].$port; +} + +/** + * Returns the absolute URL of the current script, without the query + * + * If the resource is "index.php", then it is removed (for better-looking URLs) + * + * @param array $server the $_SERVER array + * + * @return string the absolute URL of the current script, without the query + */ +function index_url($server) +{ + $scriptname = $server['SCRIPT_NAME']; + if (endsWith($scriptname, 'index.php')) { + $scriptname = substr($scriptname, 0, -9); + } + return server_url($server) . $scriptname; +} + +/** + * Returns the absolute URL of the current script, with the query + * + * If the resource is "index.php", then it is removed (for better-looking URLs) + * + * @param array $server the $_SERVER array + * + * @return string the absolute URL of the current script, with the query + */ +function page_url($server) +{ + if (! empty($server['QUERY_STRING'])) { + return index_url($server).'?'.$server['QUERY_STRING']; + } + return index_url($server); +} + +/** + * Retrieve the initial IP forwarded by the reverse proxy. + * + * Inspired from: https://github.com/zendframework/zend-http/blob/master/src/PhpEnvironment/RemoteAddress.php + * + * @param array $server $_SERVER array which contains HTTP headers. + * @param array $trustedIps List of trusted IP from the configuration. + * + * @return string|bool The forwarded IP, or false if none could be extracted. + */ +function getIpAddressFromProxy($server, $trustedIps) +{ + $forwardedIpHeader = 'HTTP_X_FORWARDED_FOR'; + if (empty($server[$forwardedIpHeader])) { + return false; + } + + $ips = preg_split('/\s*,\s*/', $server[$forwardedIpHeader]); + $ips = array_diff($ips, $trustedIps); + if (empty($ips)) { + return false; + } + + return array_pop($ips); +} + + +/** + * Return an identifier based on the advertised client IP address(es) + * + * This aims at preventing session hijacking from users behind the same proxy + * by relying on HTTP headers. + * + * See: + * - https://secure.php.net/manual/en/reserved.variables.server.php + * - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php + * - https://stackoverflow.com/questions/12233406/preventing-session-hijacking + * - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor + * + * @param array $server The $_SERVER array + * + * @return string An identifier based on client IP address information + */ +function client_ip_id($server) +{ + $ip = $server['REMOTE_ADDR']; + + if (isset($server['HTTP_X_FORWARDED_FOR'])) { + $ip = $ip . '_' . $server['HTTP_X_FORWARDED_FOR']; + } + if (isset($server['HTTP_CLIENT_IP'])) { + $ip = $ip . '_' . $server['HTTP_CLIENT_IP']; + } + return $ip; +} + + +/** + * Returns true if Shaarli's currently browsed in HTTPS. + * Supports reverse proxies (if the headers are correctly set). + * + * @param array $server $_SERVER. + * + * @return bool true if HTTPS, false otherwise. + */ +function is_https($server) +{ + + if (isset($server['HTTP_X_FORWARDED_PORT'])) { + // Keep forwarded port + if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) { + $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']); + $port = trim($ports[0]); + } else { + $port = $server['HTTP_X_FORWARDED_PORT']; + } + + if ($port == '443') { + return true; + } + } + + return ! empty($server['HTTPS']); +} diff --git a/application/http/Url.php b/application/http/Url.php index 260231c6..90444a2f 100644 --- a/application/http/Url.php +++ b/application/http/Url.php @@ -206,7 +206,7 @@ class Url } /** - * Test if the Url is an HTTP one. + * Test if the UrlUtils is an HTTP one. * * @return true is HTTP, false otherwise. */ diff --git a/application/http/UrlUtils.php b/application/http/UrlUtils.php new file mode 100644 index 00000000..4bc84b82 --- /dev/null +++ b/application/http/UrlUtils.php @@ -0,0 +1,88 @@ +cleanup(); +} + +/** + * Get URL scheme. + * + * @param string url UrlUtils for which the scheme is requested + * + * @return mixed the URL scheme or false if none is provided. + */ +function get_url_scheme($url) +{ + $obj_url = new \Shaarli\Http\Url($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, '/') ? '/' : ''); +} + +/** + * Replace not whitelisted protocols by 'http://' from given URL. + * + * @param string $url URL to clean + * @param array $protocols List of allowed protocols (aside from http(s)). + * + * @return string URL with allowed protocol + */ +function whitelist_protocols($url, $protocols) +{ + if (startsWith($url, '?') || startsWith($url, '/')) { + return $url; + } + $protocols = array_merge(['http', 'https'], $protocols); + $protocol = preg_match('#^(\w+):/?/?#', $url, $match); + // Protocol not allowed: we remove it and replace it with http + if ($protocol === 1 && ! in_array($match[1], $protocols)) { + $url = str_replace($match[0], 'http://', $url); + } elseif ($protocol !== 1) { + $url = 'http://' . $url; + } + return $url; +} -- cgit v1.2.3 From 8c0f19c7971e1a4534347ce9d6d82a0a45799711 Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Mon, 3 Dec 2018 00:46:04 +0100 Subject: namespacing: \Shaarli\Render\{PageBuilder,ThemeUtils} Signed-off-by: VirtualTam --- application/PageBuilder.php | 205 ----------------------------------- application/ThemeUtils.php | 34 ------ application/render/PageBuilder.php | 213 +++++++++++++++++++++++++++++++++++++ application/render/ThemeUtils.php | 34 ++++++ 4 files changed, 247 insertions(+), 239 deletions(-) delete mode 100644 application/PageBuilder.php delete mode 100644 application/ThemeUtils.php create mode 100644 application/render/PageBuilder.php create mode 100644 application/render/ThemeUtils.php (limited to 'application') diff --git a/application/PageBuilder.php b/application/PageBuilder.php deleted file mode 100644 index 2ca95832..00000000 --- a/application/PageBuilder.php +++ /dev/null @@ -1,205 +0,0 @@ -assign('myfield','myvalue'); - * $p->renderPage('mytemplate'); - */ -class PageBuilder -{ - /** - * @var RainTPL RainTPL instance. - */ - private $tpl; - - /** - * @var ConfigManager $conf Configuration Manager instance. - */ - protected $conf; - - /** - * @var array $_SESSION - */ - protected $session; - - /** - * @var LinkDB $linkDB instance. - */ - protected $linkDB; - - /** - * @var null|string XSRF token - */ - protected $token; - - /** @var bool $isLoggedIn Whether the user is logged in **/ - protected $isLoggedIn = false; - - /** - * PageBuilder constructor. - * $tpl is initialized at false for lazy loading. - * - * @param ConfigManager $conf Configuration Manager instance (reference). - * @param array $session $_SESSION array - * @param LinkDB $linkDB instance. - * @param string $token Session token - * @param bool $isLoggedIn - */ - public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) - { - $this->tpl = false; - $this->conf = $conf; - $this->session = $session; - $this->linkDB = $linkDB; - $this->token = $token; - $this->isLoggedIn = $isLoggedIn; - } - - /** - * Initialize all default tpl tags. - */ - private function initialize() - { - $this->tpl = new RainTPL(); - - try { - $version = ApplicationUtils::checkUpdate( - SHAARLI_VERSION, - $this->conf->get('resource.update_check'), - $this->conf->get('updates.check_updates_interval'), - $this->conf->get('updates.check_updates'), - $this->isLoggedIn, - $this->conf->get('updates.check_updates_branch') - ); - $this->tpl->assign('newVersion', escape($version)); - $this->tpl->assign('versionError', ''); - } catch (Exception $exc) { - logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); - $this->tpl->assign('newVersion', ''); - $this->tpl->assign('versionError', escape($exc->getMessage())); - } - - $this->tpl->assign('is_logged_in', $this->isLoggedIn); - $this->tpl->assign('feedurl', escape(index_url($_SERVER))); - $searchcrits = ''; // Search criteria - if (!empty($_GET['searchtags'])) { - $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']); - } - if (!empty($_GET['searchterm'])) { - $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']); - } - $this->tpl->assign('searchcrits', $searchcrits); - $this->tpl->assign('source', index_url($_SERVER)); - $this->tpl->assign('version', SHAARLI_VERSION); - $this->tpl->assign( - 'version_hash', - ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt')) - ); - $this->tpl->assign('index_url', index_url($_SERVER)); - $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; - $this->tpl->assign('visibility', $visibility); - $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly'])); - $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli')); - if ($this->conf->exists('general.header_link')) { - $this->tpl->assign('titleLink', $this->conf->get('general.header_link')); - } - $this->tpl->assign('shaarlititle', $this->conf->get('general.title', 'Shaarli')); - $this->tpl->assign('openshaarli', $this->conf->get('security.open_shaarli', false)); - $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true)); - $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); - $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); - $this->tpl->assign('token', $this->token); - - if ($this->linkDB !== null) { - $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); - } - - $this->tpl->assign( - 'thumbnails_enabled', - $this->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE - ); - $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); - $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); - - if (! empty($_SESSION['warnings'])) { - $this->tpl->assign('global_warnings', $_SESSION['warnings']); - unset($_SESSION['warnings']); - } - - // To be removed with a proper theme configuration. - $this->tpl->assign('conf', $this->conf); - } - - /** - * The following assign() method is basically the same as RainTPL (except lazy loading) - * - * @param string $placeholder Template placeholder. - * @param mixed $value Value to assign. - */ - public function assign($placeholder, $value) - { - if ($this->tpl === false) { - $this->initialize(); - } - $this->tpl->assign($placeholder, $value); - } - - /** - * Assign an array of data to the template builder. - * - * @param array $data Data to assign. - * - * @return false if invalid data. - */ - public function assignAll($data) - { - if ($this->tpl === false) { - $this->initialize(); - } - - if (empty($data) || !is_array($data)) { - return false; - } - - foreach ($data as $key => $value) { - $this->assign($key, $value); - } - return true; - } - - /** - * Render a specific page (using a template file). - * e.g. $pb->renderPage('picwall'); - * - * @param string $page Template filename (without extension). - */ - public function renderPage($page) - { - if ($this->tpl === false) { - $this->initialize(); - } - - $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 = '') - { - if (empty($message)) { - $message = t('The page you are trying to reach does not exist or has been deleted.'); - } - header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found')); - $this->tpl->assign('error_message', $message); - $this->renderPage('404'); - } -} diff --git a/application/ThemeUtils.php b/application/ThemeUtils.php deleted file mode 100644 index 16f2f6a2..00000000 --- a/application/ThemeUtils.php +++ /dev/null @@ -1,34 +0,0 @@ -assign('myfield','myvalue'); + * $p->renderPage('mytemplate'); + */ +class PageBuilder +{ + /** + * @var RainTPL RainTPL instance. + */ + private $tpl; + + /** + * @var ConfigManager $conf Configuration Manager instance. + */ + protected $conf; + + /** + * @var array $_SESSION + */ + protected $session; + + /** + * @var LinkDB $linkDB instance. + */ + protected $linkDB; + + /** + * @var null|string XSRF token + */ + protected $token; + + /** + * @var bool $isLoggedIn Whether the user is logged in + */ + protected $isLoggedIn = false; + + /** + * PageBuilder constructor. + * $tpl is initialized at false for lazy loading. + * + * @param ConfigManager $conf Configuration Manager instance (reference). + * @param array $session $_SESSION array + * @param LinkDB $linkDB instance. + * @param string $token Session token + * @param bool $isLoggedIn + */ + public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false) + { + $this->tpl = false; + $this->conf = $conf; + $this->session = $session; + $this->linkDB = $linkDB; + $this->token = $token; + $this->isLoggedIn = $isLoggedIn; + } + + /** + * Initialize all default tpl tags. + */ + private function initialize() + { + $this->tpl = new RainTPL(); + + try { + $version = ApplicationUtils::checkUpdate( + SHAARLI_VERSION, + $this->conf->get('resource.update_check'), + $this->conf->get('updates.check_updates_interval'), + $this->conf->get('updates.check_updates'), + $this->isLoggedIn, + $this->conf->get('updates.check_updates_branch') + ); + $this->tpl->assign('newVersion', escape($version)); + $this->tpl->assign('versionError', ''); + } catch (Exception $exc) { + logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage()); + $this->tpl->assign('newVersion', ''); + $this->tpl->assign('versionError', escape($exc->getMessage())); + } + + $this->tpl->assign('is_logged_in', $this->isLoggedIn); + $this->tpl->assign('feedurl', escape(index_url($_SERVER))); + $searchcrits = ''; // Search criteria + if (!empty($_GET['searchtags'])) { + $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']); + } + if (!empty($_GET['searchterm'])) { + $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']); + } + $this->tpl->assign('searchcrits', $searchcrits); + $this->tpl->assign('source', index_url($_SERVER)); + $this->tpl->assign('version', SHAARLI_VERSION); + $this->tpl->assign( + 'version_hash', + ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt')) + ); + $this->tpl->assign('index_url', index_url($_SERVER)); + $visibility = !empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; + $this->tpl->assign('visibility', $visibility); + $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly'])); + $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli')); + if ($this->conf->exists('general.header_link')) { + $this->tpl->assign('titleLink', $this->conf->get('general.header_link')); + } + $this->tpl->assign('shaarlititle', $this->conf->get('general.title', 'Shaarli')); + $this->tpl->assign('openshaarli', $this->conf->get('security.open_shaarli', false)); + $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true)); + $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); + $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); + $this->tpl->assign('token', $this->token); + + if ($this->linkDB !== null) { + $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); + } + + $this->tpl->assign( + 'thumbnails_enabled', + $this->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE + ); + $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); + $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); + + if (!empty($_SESSION['warnings'])) { + $this->tpl->assign('global_warnings', $_SESSION['warnings']); + unset($_SESSION['warnings']); + } + + // To be removed with a proper theme configuration. + $this->tpl->assign('conf', $this->conf); + } + + /** + * The following assign() method is basically the same as RainTPL (except lazy loading) + * + * @param string $placeholder Template placeholder. + * @param mixed $value Value to assign. + */ + public function assign($placeholder, $value) + { + if ($this->tpl === false) { + $this->initialize(); + } + $this->tpl->assign($placeholder, $value); + } + + /** + * Assign an array of data to the template builder. + * + * @param array $data Data to assign. + * + * @return false if invalid data. + */ + public function assignAll($data) + { + if ($this->tpl === false) { + $this->initialize(); + } + + if (empty($data) || !is_array($data)) { + return false; + } + + foreach ($data as $key => $value) { + $this->assign($key, $value); + } + return true; + } + + /** + * Render a specific page (using a template file). + * e.g. $pb->renderPage('picwall'); + * + * @param string $page Template filename (without extension). + */ + public function renderPage($page) + { + if ($this->tpl === false) { + $this->initialize(); + } + + $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 message to display what is not found + */ + public function render404($message = '') + { + if (empty($message)) { + $message = t('The page you are trying to reach does not exist or has been deleted.'); + } + header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found')); + $this->tpl->assign('error_message', $message); + $this->renderPage('404'); + } +} diff --git a/application/render/ThemeUtils.php b/application/render/ThemeUtils.php new file mode 100644 index 00000000..86096c64 --- /dev/null +++ b/application/render/ThemeUtils.php @@ -0,0 +1,34 @@ + Date: Mon, 3 Dec 2018 00:59:21 +0100 Subject: namespacing: \Shaarli\FileUtils Signed-off-by: VirtualTam --- application/FileUtils.php | 8 ++++---- application/History.php | 5 ++--- application/LinkDB.php | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'application') diff --git a/application/FileUtils.php b/application/FileUtils.php index ba409821..30560bfc 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php @@ -1,8 +1,8 @@ Date: Mon, 3 Dec 2018 01:10:39 +0100 Subject: namespacing: \Shaarli\Bookmark\LinkDB Signed-off-by: VirtualTam --- application/LinkDB.php | 591 ------------------------- application/LinkFilter.php | 2 + application/LinkUtils.php | 2 + application/NetscapeBookmarkUtils.php | 1 + application/Updater.php | 2 + application/api/ApiMiddleware.php | 2 +- application/api/controllers/ApiController.php | 2 +- application/bookmark/LinkDB.php | 601 ++++++++++++++++++++++++++ application/feed/CachedPage.php | 1 + application/feed/FeedBuilder.php | 15 +- application/render/PageBuilder.php | 2 +- 11 files changed, 620 insertions(+), 601 deletions(-) delete mode 100644 application/LinkDB.php create mode 100644 application/bookmark/LinkDB.php (limited to 'application') diff --git a/application/LinkDB.php b/application/LinkDB.php deleted file mode 100644 index a5b42727..00000000 --- a/application/LinkDB.php +++ /dev/null @@ -1,591 +0,0 @@ -link offset) - private $urls; - - /** - * @var array List of all links IDS mapped with their array offset. - * Map: id->offset. - */ - protected $ids; - - // List of offset keys (for the Iterator interface implementation) - private $keys; - - // Position in the $this->keys array (for the Iterator interface) - private $position; - - // Is the user logged in? (used to filter private links) - private $loggedIn; - - // Hide public links - private $hidePublicLinks; - - // link redirector set in user settings. - private $redirector; - - /** - * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched. - * - * Example: - * anonym.to needs clean URL while dereferer.org needs urlencoded URL. - * - * @var boolean $redirectorEncode parameter: true or false - */ - private $redirectorEncode; - - /** - * Creates a new LinkDB - * - * Checks if the datastore exists; else, attempts to create a dummy one. - * - * @param string $datastore datastore file path. - * @param boolean $isLoggedIn is the user logged in? - * @param boolean $hidePublicLinks if true all links are private. - * @param string $redirector link redirector set in user settings. - * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true). - */ - public function __construct( - $datastore, - $isLoggedIn, - $hidePublicLinks, - $redirector = '', - $redirectorEncode = true - ) { - $this->datastore = $datastore; - $this->loggedIn = $isLoggedIn; - $this->hidePublicLinks = $hidePublicLinks; - $this->redirector = $redirector; - $this->redirectorEncode = $redirectorEncode === true; - $this->check(); - $this->read(); - } - - /** - * Countable - Counts elements of an object - */ - public function count() - { - return count($this->links); - } - - /** - * ArrayAccess - Assigns a value to the specified offset - */ - public function offsetSet($offset, $value) - { - // TODO: use exceptions instead of "die" - if (!$this->loggedIn) { - die(t('You are not authorized to add a link.')); - } - if (!isset($value['id']) || empty($value['url'])) { - die(t('Internal Error: A link should always have an id and URL.')); - } - if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { - die(t('You must specify an integer as a key.')); - } - if ($offset !== null && $offset !== $value['id']) { - die(t('Array offset and link ID must be equal.')); - } - - // If the link exists, we reuse the real offset, otherwise new entry - $existing = $this->getLinkOffset($offset); - if ($existing !== null) { - $offset = $existing; - } else { - $offset = count($this->links); - } - $this->links[$offset] = $value; - $this->urls[$value['url']] = $offset; - $this->ids[$value['id']] = $offset; - } - - /** - * ArrayAccess - Whether or not an offset exists - */ - public function offsetExists($offset) - { - return array_key_exists($this->getLinkOffset($offset), $this->links); - } - - /** - * ArrayAccess - Unsets an offset - */ - public function offsetUnset($offset) - { - if (!$this->loggedIn) { - // TODO: raise an exception - die('You are not authorized to delete a link.'); - } - $realOffset = $this->getLinkOffset($offset); - $url = $this->links[$realOffset]['url']; - unset($this->urls[$url]); - unset($this->ids[$realOffset]); - unset($this->links[$realOffset]); - } - - /** - * ArrayAccess - Returns the value at specified offset - */ - public function offsetGet($offset) - { - $realOffset = $this->getLinkOffset($offset); - return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null; - } - - /** - * Iterator - Returns the current element - */ - public function current() - { - return $this[$this->keys[$this->position]]; - } - - /** - * Iterator - Returns the key of the current element - */ - public function key() - { - return $this->keys[$this->position]; - } - - /** - * Iterator - Moves forward to next element - */ - public function next() - { - ++$this->position; - } - - /** - * Iterator - Rewinds the Iterator to the first element - * - * Entries are sorted by date (latest first) - */ - public function rewind() - { - $this->keys = array_keys($this->ids); - $this->position = 0; - } - - /** - * Iterator - Checks if current position is valid - */ - public function valid() - { - return isset($this->keys[$this->position]); - } - - /** - * Checks if the DB directory and file exist - * - * If no DB file is found, creates a dummy DB. - */ - private function check() - { - if (file_exists($this->datastore)) { - return; - } - - // Create a dummy database for example - $this->links = array(); - $link = array( - 'id' => 1, - 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'), - 'url'=>'https://shaarli.readthedocs.io', - 'description'=>t( - 'Welcome to Shaarli! This is your first public bookmark. ' - .'To edit or delete me, you must first login. - -To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. - -You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' - ), - 'private'=>0, - 'created'=> new DateTime(), - 'tags'=>'opensource software' - ); - $link['shorturl'] = link_small_hash($link['created'], $link['id']); - $this->links[1] = $link; - - $link = array( - 'id' => 0, - 'title'=> t('My secret stuff... - Pastebin.com'), - 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', - 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'), - 'private'=>1, - 'created'=> new DateTime('1 minute ago'), - 'tags'=>'secretstuff', - ); - $link['shorturl'] = link_small_hash($link['created'], $link['id']); - $this->links[0] = $link; - - // Write database to disk - $this->write(); - } - - /** - * Reads database from disk to memory - */ - private function read() - { - // Public links are hidden and user not logged in => nothing to show - if ($this->hidePublicLinks && !$this->loggedIn) { - $this->links = array(); - return; - } - - $this->urls = []; - $this->ids = []; - $this->links = FileUtils::readFlatDB($this->datastore, []); - - $toremove = array(); - foreach ($this->links as $key => &$link) { - if (! $this->loggedIn && $link['private'] != 0) { - // Transition for not upgraded databases. - unset($this->links[$key]); - continue; - } - - // Sanitize data fields. - sanitizeLink($link); - - // Remove private tags if the user is not logged in. - if (! $this->loggedIn) { - $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']); - } - - // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). - if (!empty($this->redirector) && !startsWith($link['url'], '?')) { - $link['real_url'] = $this->redirector; - if ($this->redirectorEncode) { - $link['real_url'] .= urlencode(unescape($link['url'])); - } else { - $link['real_url'] .= $link['url']; - } - } else { - $link['real_url'] = $link['url']; - } - - // To be able to load links before running the update, and prepare the update - if (! isset($link['created'])) { - $link['id'] = $link['linkdate']; - $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); - if (! empty($link['updated'])) { - $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']); - } - $link['shorturl'] = smallHash($link['linkdate']); - } - - $this->urls[$link['url']] = $key; - $this->ids[$link['id']] = $key; - } - } - - /** - * Saves the database from memory to disk - * - * @throws IOException the datastore is not writable - */ - private function write() - { - $this->reorder(); - FileUtils::writeFlatDB($this->datastore, $this->links); - } - - /** - * Saves the database from memory to disk - * - * @param string $pageCacheDir page cache directory - */ - public function save($pageCacheDir) - { - if (!$this->loggedIn) { - // TODO: raise an Exception instead - die('You are not authorized to change the database.'); - } - - $this->write(); - - invalidateCaches($pageCacheDir); - } - - /** - * Returns the link for a given URL, or False if it does not exist. - * - * @param string $url URL to search for - * - * @return mixed the existing link if it exists, else 'false' - */ - public function getLinkFromUrl($url) - { - if (isset($this->urls[$url])) { - return $this->links[$this->urls[$url]]; - } - return false; - } - - /** - * Returns the shaare corresponding to a smallHash. - * - * @param string $request QUERY_STRING server parameter. - * - * @return array $filtered array containing permalink data. - * - * @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link. - */ - public function filterHash($request) - { - $request = substr($request, 0, 6); - $linkFilter = new LinkFilter($this->links); - return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); - } - - /** - * Returns the list of articles for a given day. - * - * @param string $request day to filter. Format: YYYYMMDD. - * - * @return array list of shaare found. - */ - public function filterDay($request) - { - $linkFilter = new LinkFilter($this->links); - return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); - } - - /** - * Filter links according to search parameters. - * - * @param array $filterRequest Search request content. Supported keys: - * - searchtags: list of tags - * - searchterm: term search - * @param bool $casesensitive Optional: Perform case sensitive filter - * @param string $visibility return only all/private/public links - * @param string $untaggedonly return only untagged links - * - * @return array filtered links, all links if no suitable filter was provided. - */ - public function filterSearch( - $filterRequest = array(), - $casesensitive = false, - $visibility = 'all', - $untaggedonly = false - ) { - // Filter link database according to parameters. - $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; - $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; - - // Search tags + fullsearch - blank string parameter will return all links. - $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext" - $request = [$searchtags, $searchterm]; - - $linkFilter = new LinkFilter($this); - return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly); - } - - /** - * Returns the list tags appearing in the links with the given tags - * - * @param array $filteringTags tags selecting the links to consider - * @param string $visibility process only all/private/public links - * - * @return array tag => linksCount - */ - public function linksCountPerTag($filteringTags = [], $visibility = 'all') - { - $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility); - $tags = []; - $caseMapping = []; - foreach ($links as $link) { - foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { - if (empty($tag)) { - continue; - } - // The first case found will be displayed. - if (!isset($caseMapping[strtolower($tag)])) { - $caseMapping[strtolower($tag)] = $tag; - $tags[$caseMapping[strtolower($tag)]] = 0; - } - $tags[$caseMapping[strtolower($tag)]]++; - } - } - - /* - * Formerly used arsort(), which doesn't define the sort behaviour for equal values. - * Also, this function doesn't produce the same result between PHP 5.6 and 7. - * - * So we now use array_multisort() to sort tags by DESC occurrences, - * then ASC alphabetically for equal values. - * - * @see https://github.com/shaarli/Shaarli/issues/1142 - */ - $keys = array_keys($tags); - $tmpTags = array_combine($keys, $keys); - array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); - return $tags; - } - - /** - * Rename or delete a tag across all links. - * - * @param string $from Tag to rename - * @param string $to New tag. If none is provided, the from tag will be deleted - * - * @return array|bool List of altered links or false on error - */ - public function renameTag($from, $to) - { - if (empty($from)) { - return false; - } - $delete = empty($to); - // True for case-sensitive tag search. - $linksToAlter = $this->filterSearch(['searchtags' => $from], true); - foreach ($linksToAlter as $key => &$value) { - $tags = preg_split('/\s+/', trim($value['tags'])); - if (($pos = array_search($from, $tags)) !== false) { - if ($delete) { - unset($tags[$pos]); // Remove tag. - } else { - $tags[$pos] = trim($to); - } - $value['tags'] = trim(implode(' ', array_unique($tags))); - $this[$value['id']] = $value; - } - } - - return $linksToAlter; - } - - /** - * Returns the list of days containing articles (oldest first) - * Output: An array containing days (in format YYYYMMDD). - */ - public function days() - { - $linkDays = array(); - foreach ($this->links as $link) { - $linkDays[$link['created']->format('Ymd')] = 0; - } - $linkDays = array_keys($linkDays); - sort($linkDays); - - return $linkDays; - } - - /** - * Reorder links by creation date (newest first). - * - * Also update the urls and ids mapping arrays. - * - * @param string $order ASC|DESC - */ - public function reorder($order = 'DESC') - { - $order = $order === 'ASC' ? -1 : 1; - // Reorder array by dates. - usort($this->links, function ($a, $b) use ($order) { - if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) { - return $a['sticky'] ? -1 : 1; - } - return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; - }); - - $this->urls = []; - $this->ids = []; - foreach ($this->links as $key => $link) { - $this->urls[$link['url']] = $key; - $this->ids[$link['id']] = $key; - } - } - - /** - * Return the next key for link creation. - * E.g. If the last ID is 597, the next will be 598. - * - * @return int next ID. - */ - public function getNextId() - { - if (!empty($this->ids)) { - return max(array_keys($this->ids)) + 1; - } - return 0; - } - - /** - * Returns a link offset in links array from its unique ID. - * - * @param int $id Persistent ID of a link. - * - * @return int Real offset in local array, or null if doesn't exist. - */ - protected function getLinkOffset($id) - { - if (isset($this->ids[$id])) { - return $this->ids[$id]; - } - return null; - } -} diff --git a/application/LinkFilter.php b/application/LinkFilter.php index 8f147974..91c79905 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php @@ -1,5 +1,7 @@ get('resource.datastore'), true, $conf->get('privacy.hide_public_links'), diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php index 47e0e178..cab97dc4 100644 --- a/application/api/controllers/ApiController.php +++ b/application/api/controllers/ApiController.php @@ -25,7 +25,7 @@ abstract class ApiController protected $conf; /** - * @var \LinkDB + * @var \Shaarli\Bookmark\LinkDB */ protected $linkDb; diff --git a/application/bookmark/LinkDB.php b/application/bookmark/LinkDB.php new file mode 100644 index 00000000..3b77422a --- /dev/null +++ b/application/bookmark/LinkDB.php @@ -0,0 +1,601 @@ +link offset) + private $urls; + + /** + * @var array List of all links IDS mapped with their array offset. + * Map: id->offset. + */ + protected $ids; + + // List of offset keys (for the Iterator interface implementation) + private $keys; + + // Position in the $this->keys array (for the Iterator interface) + private $position; + + // Is the user logged in? (used to filter private links) + private $loggedIn; + + // Hide public links + private $hidePublicLinks; + + // link redirector set in user settings. + private $redirector; + + /** + * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched. + * + * Example: + * anonym.to needs clean URL while dereferer.org needs urlencoded URL. + * + * @var boolean $redirectorEncode parameter: true or false + */ + private $redirectorEncode; + + /** + * Creates a new LinkDB + * + * Checks if the datastore exists; else, attempts to create a dummy one. + * + * @param string $datastore datastore file path. + * @param boolean $isLoggedIn is the user logged in? + * @param boolean $hidePublicLinks if true all links are private. + * @param string $redirector link redirector set in user settings. + * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true). + */ + public function __construct( + $datastore, + $isLoggedIn, + $hidePublicLinks, + $redirector = '', + $redirectorEncode = true + ) { + + $this->datastore = $datastore; + $this->loggedIn = $isLoggedIn; + $this->hidePublicLinks = $hidePublicLinks; + $this->redirector = $redirector; + $this->redirectorEncode = $redirectorEncode === true; + $this->check(); + $this->read(); + } + + /** + * Countable - Counts elements of an object + */ + public function count() + { + return count($this->links); + } + + /** + * ArrayAccess - Assigns a value to the specified offset + */ + public function offsetSet($offset, $value) + { + // TODO: use exceptions instead of "die" + if (!$this->loggedIn) { + die(t('You are not authorized to add a link.')); + } + if (!isset($value['id']) || empty($value['url'])) { + die(t('Internal Error: A link should always have an id and URL.')); + } + if (($offset !== null && !is_int($offset)) || !is_int($value['id'])) { + die(t('You must specify an integer as a key.')); + } + if ($offset !== null && $offset !== $value['id']) { + die(t('Array offset and link ID must be equal.')); + } + + // If the link exists, we reuse the real offset, otherwise new entry + $existing = $this->getLinkOffset($offset); + if ($existing !== null) { + $offset = $existing; + } else { + $offset = count($this->links); + } + $this->links[$offset] = $value; + $this->urls[$value['url']] = $offset; + $this->ids[$value['id']] = $offset; + } + + /** + * ArrayAccess - Whether or not an offset exists + */ + public function offsetExists($offset) + { + return array_key_exists($this->getLinkOffset($offset), $this->links); + } + + /** + * ArrayAccess - Unsets an offset + */ + public function offsetUnset($offset) + { + if (!$this->loggedIn) { + // TODO: raise an exception + die('You are not authorized to delete a link.'); + } + $realOffset = $this->getLinkOffset($offset); + $url = $this->links[$realOffset]['url']; + unset($this->urls[$url]); + unset($this->ids[$realOffset]); + unset($this->links[$realOffset]); + } + + /** + * ArrayAccess - Returns the value at specified offset + */ + public function offsetGet($offset) + { + $realOffset = $this->getLinkOffset($offset); + return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null; + } + + /** + * Iterator - Returns the current element + */ + public function current() + { + return $this[$this->keys[$this->position]]; + } + + /** + * Iterator - Returns the key of the current element + */ + public function key() + { + return $this->keys[$this->position]; + } + + /** + * Iterator - Moves forward to next element + */ + public function next() + { + ++$this->position; + } + + /** + * Iterator - Rewinds the Iterator to the first element + * + * Entries are sorted by date (latest first) + */ + public function rewind() + { + $this->keys = array_keys($this->ids); + $this->position = 0; + } + + /** + * Iterator - Checks if current position is valid + */ + public function valid() + { + return isset($this->keys[$this->position]); + } + + /** + * Checks if the DB directory and file exist + * + * If no DB file is found, creates a dummy DB. + */ + private function check() + { + if (file_exists($this->datastore)) { + return; + } + + // Create a dummy database for example + $this->links = array(); + $link = array( + 'id' => 1, + 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'), + 'url' => 'https://shaarli.readthedocs.io', + 'description' => t( + 'Welcome to Shaarli! This is your first public bookmark. ' + . 'To edit or delete me, you must first login. + +To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. + +You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' + ), + 'private' => 0, + 'created' => new DateTime(), + 'tags' => 'opensource software' + ); + $link['shorturl'] = link_small_hash($link['created'], $link['id']); + $this->links[1] = $link; + + $link = array( + 'id' => 0, + 'title' => t('My secret stuff... - Pastebin.com'), + 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', + 'description' => t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'), + 'private' => 1, + 'created' => new DateTime('1 minute ago'), + 'tags' => 'secretstuff', + ); + $link['shorturl'] = link_small_hash($link['created'], $link['id']); + $this->links[0] = $link; + + // Write database to disk + $this->write(); + } + + /** + * Reads database from disk to memory + */ + private function read() + { + // Public links are hidden and user not logged in => nothing to show + if ($this->hidePublicLinks && !$this->loggedIn) { + $this->links = array(); + return; + } + + $this->urls = []; + $this->ids = []; + $this->links = FileUtils::readFlatDB($this->datastore, []); + + $toremove = array(); + foreach ($this->links as $key => &$link) { + if (!$this->loggedIn && $link['private'] != 0) { + // Transition for not upgraded databases. + unset($this->links[$key]); + continue; + } + + // Sanitize data fields. + sanitizeLink($link); + + // Remove private tags if the user is not logged in. + if (!$this->loggedIn) { + $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']); + } + + // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). + if (!empty($this->redirector) && !startsWith($link['url'], '?')) { + $link['real_url'] = $this->redirector; + if ($this->redirectorEncode) { + $link['real_url'] .= urlencode(unescape($link['url'])); + } else { + $link['real_url'] .= $link['url']; + } + } else { + $link['real_url'] = $link['url']; + } + + // To be able to load links before running the update, and prepare the update + if (!isset($link['created'])) { + $link['id'] = $link['linkdate']; + $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); + if (!empty($link['updated'])) { + $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']); + } + $link['shorturl'] = smallHash($link['linkdate']); + } + + $this->urls[$link['url']] = $key; + $this->ids[$link['id']] = $key; + } + } + + /** + * Saves the database from memory to disk + * + * @throws IOException the datastore is not writable + */ + private function write() + { + $this->reorder(); + FileUtils::writeFlatDB($this->datastore, $this->links); + } + + /** + * Saves the database from memory to disk + * + * @param string $pageCacheDir page cache directory + */ + public function save($pageCacheDir) + { + if (!$this->loggedIn) { + // TODO: raise an Exception instead + die('You are not authorized to change the database.'); + } + + $this->write(); + + invalidateCaches($pageCacheDir); + } + + /** + * Returns the link for a given URL, or False if it does not exist. + * + * @param string $url URL to search for + * + * @return mixed the existing link if it exists, else 'false' + */ + public function getLinkFromUrl($url) + { + if (isset($this->urls[$url])) { + return $this->links[$this->urls[$url]]; + } + return false; + } + + /** + * Returns the shaare corresponding to a smallHash. + * + * @param string $request QUERY_STRING server parameter. + * + * @return array $filtered array containing permalink data. + * + * @throws LinkNotFoundException if the smallhash is malformed or doesn't match any link. + */ + public function filterHash($request) + { + $request = substr($request, 0, 6); + $linkFilter = new LinkFilter($this->links); + return $linkFilter->filter(LinkFilter::$FILTER_HASH, $request); + } + + /** + * Returns the list of articles for a given day. + * + * @param string $request day to filter. Format: YYYYMMDD. + * + * @return array list of shaare found. + */ + public function filterDay($request) + { + $linkFilter = new LinkFilter($this->links); + return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request); + } + + /** + * Filter links according to search parameters. + * + * @param array $filterRequest Search request content. Supported keys: + * - searchtags: list of tags + * - searchterm: term search + * @param bool $casesensitive Optional: Perform case sensitive filter + * @param string $visibility return only all/private/public links + * @param string $untaggedonly return only untagged links + * + * @return array filtered links, all links if no suitable filter was provided. + */ + public function filterSearch( + $filterRequest = array(), + $casesensitive = false, + $visibility = 'all', + $untaggedonly = false + ) { + + // Filter link database according to parameters. + $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; + $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; + + // Search tags + fullsearch - blank string parameter will return all links. + $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext" + $request = [$searchtags, $searchterm]; + + $linkFilter = new LinkFilter($this); + return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly); + } + + /** + * Returns the list tags appearing in the links with the given tags + * + * @param array $filteringTags tags selecting the links to consider + * @param string $visibility process only all/private/public links + * + * @return array tag => linksCount + */ + public function linksCountPerTag($filteringTags = [], $visibility = 'all') + { + $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility); + $tags = []; + $caseMapping = []; + foreach ($links as $link) { + foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) { + if (empty($tag)) { + continue; + } + // The first case found will be displayed. + if (!isset($caseMapping[strtolower($tag)])) { + $caseMapping[strtolower($tag)] = $tag; + $tags[$caseMapping[strtolower($tag)]] = 0; + } + $tags[$caseMapping[strtolower($tag)]]++; + } + } + + /* + * Formerly used arsort(), which doesn't define the sort behaviour for equal values. + * Also, this function doesn't produce the same result between PHP 5.6 and 7. + * + * So we now use array_multisort() to sort tags by DESC occurrences, + * then ASC alphabetically for equal values. + * + * @see https://github.com/shaarli/Shaarli/issues/1142 + */ + $keys = array_keys($tags); + $tmpTags = array_combine($keys, $keys); + array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags); + return $tags; + } + + /** + * Rename or delete a tag across all links. + * + * @param string $from Tag to rename + * @param string $to New tag. If none is provided, the from tag will be deleted + * + * @return array|bool List of altered links or false on error + */ + public function renameTag($from, $to) + { + if (empty($from)) { + return false; + } + $delete = empty($to); + // True for case-sensitive tag search. + $linksToAlter = $this->filterSearch(['searchtags' => $from], true); + foreach ($linksToAlter as $key => &$value) { + $tags = preg_split('/\s+/', trim($value['tags'])); + if (($pos = array_search($from, $tags)) !== false) { + if ($delete) { + unset($tags[$pos]); // Remove tag. + } else { + $tags[$pos] = trim($to); + } + $value['tags'] = trim(implode(' ', array_unique($tags))); + $this[$value['id']] = $value; + } + } + + return $linksToAlter; + } + + /** + * Returns the list of days containing articles (oldest first) + * Output: An array containing days (in format YYYYMMDD). + */ + public function days() + { + $linkDays = array(); + foreach ($this->links as $link) { + $linkDays[$link['created']->format('Ymd')] = 0; + } + $linkDays = array_keys($linkDays); + sort($linkDays); + + return $linkDays; + } + + /** + * Reorder links by creation date (newest first). + * + * Also update the urls and ids mapping arrays. + * + * @param string $order ASC|DESC + */ + public function reorder($order = 'DESC') + { + $order = $order === 'ASC' ? -1 : 1; + // Reorder array by dates. + usort($this->links, function ($a, $b) use ($order) { + if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) { + return $a['sticky'] ? -1 : 1; + } + return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; + }); + + $this->urls = []; + $this->ids = []; + foreach ($this->links as $key => $link) { + $this->urls[$link['url']] = $key; + $this->ids[$link['id']] = $key; + } + } + + /** + * Return the next key for link creation. + * E.g. If the last ID is 597, the next will be 598. + * + * @return int next ID. + */ + public function getNextId() + { + if (!empty($this->ids)) { + return max(array_keys($this->ids)) + 1; + } + return 0; + } + + /** + * Returns a link offset in links array from its unique ID. + * + * @param int $id Persistent ID of a link. + * + * @return int Real offset in local array, or null if doesn't exist. + */ + protected function getLinkOffset($id) + { + if (isset($this->ids[$id])) { + return $this->ids[$id]; + } + return null; + } +} diff --git a/application/feed/CachedPage.php b/application/feed/CachedPage.php index 1c51ac73..d809bdd9 100644 --- a/application/feed/CachedPage.php +++ b/application/feed/CachedPage.php @@ -1,6 +1,7 @@ Date: Mon, 3 Dec 2018 01:22:45 +0100 Subject: namespacing: \Shaarli\Bookmark\LinkFilter Signed-off-by: VirtualTam --- application/LinkFilter.php | 455 --------------------- application/Updater.php | 1 + application/bookmark/LinkDB.php | 26 +- application/bookmark/LinkFilter.php | 449 ++++++++++++++++++++ .../bookmark/exception/LinkNotFoundException.php | 15 + 5 files changed, 478 insertions(+), 468 deletions(-) delete mode 100644 application/LinkFilter.php create mode 100644 application/bookmark/LinkFilter.php create mode 100644 application/bookmark/exception/LinkNotFoundException.php (limited to 'application') diff --git a/application/LinkFilter.php b/application/LinkFilter.php deleted file mode 100644 index 91c79905..00000000 --- a/application/LinkFilter.php +++ /dev/null @@ -1,455 +0,0 @@ -links = $links; - } - - /** - * Filter links according to parameters. - * - * @param string $type Type of filter (eg. tags, permalink, etc.). - * @param mixed $request Filter content. - * @param bool $casesensitive Optional: Perform case sensitive filter if true. - * @param string $visibility Optional: return only all/private/public links - * @param string $untaggedonly Optional: return only untagged links. Applies only if $type includes FILTER_TAG - * - * @return array filtered link list. - */ - public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) - { - if (! in_array($visibility, ['all', 'public', 'private'])) { - $visibility = 'all'; - } - - switch ($type) { - case self::$FILTER_HASH: - return $this->filterSmallHash($request); - case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext" - $noRequest = empty($request) || (empty($request[0]) && empty($request[1])); - if ($noRequest) { - if ($untaggedonly) { - return $this->filterUntagged($visibility); - } - return $this->noFilter($visibility); - } - if ($untaggedonly) { - $filtered = $this->filterUntagged($visibility); - } else { - $filtered = $this->links; - } - if (!empty($request[0])) { - $filtered = (new LinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); - } - if (!empty($request[1])) { - $filtered = (new LinkFilter($filtered))->filterFulltext($request[1], $visibility); - } - return $filtered; - case self::$FILTER_TEXT: - return $this->filterFulltext($request, $visibility); - case self::$FILTER_TAG: - if ($untaggedonly) { - return $this->filterUntagged($visibility); - } else { - return $this->filterTags($request, $casesensitive, $visibility); - } - case self::$FILTER_DAY: - return $this->filterDay($request); - default: - return $this->noFilter($visibility); - } - } - - /** - * Unknown filter, but handle private only. - * - * @param string $visibility Optional: return only all/private/public links - * - * @return array filtered links. - */ - private function noFilter($visibility = 'all') - { - if ($visibility === 'all') { - return $this->links; - } - - $out = array(); - foreach ($this->links as $key => $value) { - if ($value['private'] && $visibility === 'private') { - $out[$key] = $value; - } elseif (! $value['private'] && $visibility === 'public') { - $out[$key] = $value; - } - } - - return $out; - } - - /** - * Returns the shaare corresponding to a smallHash. - * - * @param string $smallHash permalink hash. - * - * @return array $filtered array containing permalink data. - * - * @throws LinkNotFoundException if the smallhash doesn't match any link. - */ - private function filterSmallHash($smallHash) - { - $filtered = array(); - foreach ($this->links as $key => $l) { - if ($smallHash == $l['shorturl']) { - // Yes, this is ugly and slow - $filtered[$key] = $l; - return $filtered; - } - } - - if (empty($filtered)) { - throw new LinkNotFoundException(); - } - - return $filtered; - } - - /** - * Returns the list of links corresponding to a full-text search - * - * Searches: - * - in the URLs, title and description; - * - are case-insensitive; - * - terms surrounded by quotes " are exact terms search. - * - terms starting with a dash - are excluded (except exact terms). - * - * 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 string $visibility Optional: return only all/private/public links. - * - * @return array search results. - */ - private function filterFulltext($searchterms, $visibility = 'all') - { - if (empty($searchterms)) { - return $this->noFilter($visibility); - } - - $filtered = array(); - $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); - $exactRegex = '/"([^"]+)"/'; - // Retrieve exact search terms. - preg_match_all($exactRegex, $search, $exactSearch); - $exactSearch = array_values(array_filter($exactSearch[1])); - - // Remove exact search terms to get AND terms search. - $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search))); - $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); - - // Filter excluding terms and update andSearch. - $excludeSearch = array(); - $andSearch = array(); - foreach ($explodedSearchAnd as $needle) { - if ($needle[0] == '-' && strlen($needle) > 1) { - $excludeSearch[] = substr($needle, 1); - } else { - $andSearch[] = $needle; - } - } - - $keys = array('title', 'description', 'url', 'tags'); - - // Iterate over every stored link. - foreach ($this->links as $id => $link) { - // ignore non private links when 'privatonly' is on. - if ($visibility !== 'all') { - if (! $link['private'] && $visibility === 'private') { - continue; - } elseif ($link['private'] && $visibility === 'public') { - continue; - } - } - - // Concatenate link fields to search across fields. - // Adds a '\' separator for exact search terms. - $content = ''; - foreach ($keys as $key) { - $content .= mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8') . '\\'; - } - - // Be optimistic - $found = true; - - // First, we look for exact term search - for ($i = 0; $i < count($exactSearch) && $found; $i++) { - $found = strpos($content, $exactSearch[$i]) !== false; - } - - // Iterate over keywords, if keyword is not found, - // no need to check for the others. We want all or nothing. - for ($i = 0; $i < count($andSearch) && $found; $i++) { - $found = strpos($content, $andSearch[$i]) !== false; - } - - // Exclude terms. - for ($i = 0; $i < count($excludeSearch) && $found; $i++) { - $found = strpos($content, $excludeSearch[$i]) === false; - } - - if ($found) { - $filtered[$id] = $link; - } - } - - return $filtered; - } - - /** - * generate a regex fragment out of a tag - * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard - * @return string generated regex fragment - */ - private static function tag2regex($tag) - { - $len = strlen($tag); - if (!$len || $tag === "-" || $tag === "*") { - // nothing to search, return empty regex - return ''; - } - if ($tag[0] === "-") { - // query is negated - $i = 1; // use offset to start after '-' character - $regex = '(?!'; // create negative lookahead - } else { - $i = 0; // start at first character - $regex = '(?='; // use positive lookahead - } - $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning - // iterate over string, separating it into placeholder and content - for (; $i < $len; $i++) { - if ($tag[$i] === '*') { - // placeholder found - $regex .= '[^ ]*?'; - } else { - // regular characters - $offset = strpos($tag, '*', $i); - if ($offset === false) { - // no placeholder found, set offset to end of string - $offset = $len; - } - // subtract one, as we want to get before the placeholder or end of string - $offset -= 1; - // we got a tag name that we want to search for. escape any regex characters to prevent conflicts. - $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/'); - // move $i on - $i = $offset; - } - } - $regex .= '(?:$| ))'; // after the tag may only be a space or the end - return $regex; - } - - /** - * 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 string $visibility Optional: return only all/private/public links. - * - * @return array filtered links. - */ - public function filterTags($tags, $casesensitive = false, $visibility = 'all') - { - // get single tags (we may get passed an array, even though the docs say different) - $inputTags = $tags; - if (!is_array($tags)) { - // we got an input string, split tags - $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); - } - - if (!count($inputTags)) { - // no input tags - return $this->noFilter($visibility); - } - - // build regex from all tags - $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; - if (!$casesensitive) { - // make regex case insensitive - $re .= 'i'; - } - - // create resulting array - $filtered = array(); - - // iterate over each link - foreach ($this->links as $key => $link) { - // check level of visibility - // ignore non private links when 'privateonly' is on. - if ($visibility !== 'all') { - if (! $link['private'] && $visibility === 'private') { - continue; - } elseif ($link['private'] && $visibility === 'public') { - continue; - } - } - $search = $link['tags']; // build search string, start with tags of current link - if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) { - // description given and at least one possible tag found - $descTags = array(); - // find all tags in the form of #tag in the description - preg_match_all( - '/(?links as $key => $link) { - if ($visibility !== 'all') { - if (! $link['private'] && $visibility === 'private') { - continue; - } elseif ($link['private'] && $visibility === 'public') { - continue; - } - } - - if (empty(trim($link['tags']))) { - $filtered[$key] = $link; - } - } - - 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 $key => $l) { - if ($l['created']->format('Ymd') == $day) { - $filtered[$key] = $l; - } - } - - // sort by date ASC - return array_reverse($filtered, true); - } - - /** - * Convert a list of tags (str) to an array. Also - * - handle case sensitivity. - * - accepts spaces commas as separator. - * - * @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 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 preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); - } -} - -class LinkNotFoundException extends Exception -{ - /** - * LinkNotFoundException constructor. - */ - public function __construct() - { - $this->message = t('The link you are trying to reach does not exist or has been deleted.'); - } -} diff --git a/application/Updater.php b/application/Updater.php index 043ecf68..ca05ecc2 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -1,6 +1,7 @@ linksCount */ @@ -500,7 +500,7 @@ You use the community supported version of the original Shaarli project, by Seba * Rename or delete a tag across all links. * * @param string $from Tag to rename - * @param string $to New tag. If none is provided, the from tag will be deleted + * @param string $to New tag. If none is provided, the from tag will be deleted * * @return array|bool List of altered links or false on error */ diff --git a/application/bookmark/LinkFilter.php b/application/bookmark/LinkFilter.php new file mode 100644 index 00000000..9b966307 --- /dev/null +++ b/application/bookmark/LinkFilter.php @@ -0,0 +1,449 @@ +links = $links; + } + + /** + * Filter links according to parameters. + * + * @param string $type Type of filter (eg. tags, permalink, etc.). + * @param mixed $request Filter content. + * @param bool $casesensitive Optional: Perform case sensitive filter if true. + * @param string $visibility Optional: return only all/private/public links + * @param string $untaggedonly Optional: return only untagged links. Applies only if $type includes FILTER_TAG + * + * @return array filtered link list. + */ + public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) + { + if (!in_array($visibility, ['all', 'public', 'private'])) { + $visibility = 'all'; + } + + switch ($type) { + case self::$FILTER_HASH: + return $this->filterSmallHash($request); + case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext" + $noRequest = empty($request) || (empty($request[0]) && empty($request[1])); + if ($noRequest) { + if ($untaggedonly) { + return $this->filterUntagged($visibility); + } + return $this->noFilter($visibility); + } + if ($untaggedonly) { + $filtered = $this->filterUntagged($visibility); + } else { + $filtered = $this->links; + } + if (!empty($request[0])) { + $filtered = (new LinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility); + } + if (!empty($request[1])) { + $filtered = (new LinkFilter($filtered))->filterFulltext($request[1], $visibility); + } + return $filtered; + case self::$FILTER_TEXT: + return $this->filterFulltext($request, $visibility); + case self::$FILTER_TAG: + if ($untaggedonly) { + return $this->filterUntagged($visibility); + } else { + return $this->filterTags($request, $casesensitive, $visibility); + } + case self::$FILTER_DAY: + return $this->filterDay($request); + default: + return $this->noFilter($visibility); + } + } + + /** + * Unknown filter, but handle private only. + * + * @param string $visibility Optional: return only all/private/public links + * + * @return array filtered links. + */ + private function noFilter($visibility = 'all') + { + if ($visibility === 'all') { + return $this->links; + } + + $out = array(); + foreach ($this->links as $key => $value) { + if ($value['private'] && $visibility === 'private') { + $out[$key] = $value; + } elseif (!$value['private'] && $visibility === 'public') { + $out[$key] = $value; + } + } + + return $out; + } + + /** + * Returns the shaare corresponding to a smallHash. + * + * @param string $smallHash permalink hash. + * + * @return array $filtered array containing permalink data. + * + * @throws \Shaarli\Bookmark\Exception\LinkNotFoundException if the smallhash doesn't match any link. + */ + private function filterSmallHash($smallHash) + { + $filtered = array(); + foreach ($this->links as $key => $l) { + if ($smallHash == $l['shorturl']) { + // Yes, this is ugly and slow + $filtered[$key] = $l; + return $filtered; + } + } + + if (empty($filtered)) { + throw new LinkNotFoundException(); + } + + return $filtered; + } + + /** + * Returns the list of links corresponding to a full-text search + * + * Searches: + * - in the URLs, title and description; + * - are case-insensitive; + * - terms surrounded by quotes " are exact terms search. + * - terms starting with a dash - are excluded (except exact terms). + * + * 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 string $visibility Optional: return only all/private/public links. + * + * @return array search results. + */ + private function filterFulltext($searchterms, $visibility = 'all') + { + if (empty($searchterms)) { + return $this->noFilter($visibility); + } + + $filtered = array(); + $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8'); + $exactRegex = '/"([^"]+)"/'; + // Retrieve exact search terms. + preg_match_all($exactRegex, $search, $exactSearch); + $exactSearch = array_values(array_filter($exactSearch[1])); + + // Remove exact search terms to get AND terms search. + $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search))); + $explodedSearchAnd = array_values(array_filter($explodedSearchAnd)); + + // Filter excluding terms and update andSearch. + $excludeSearch = array(); + $andSearch = array(); + foreach ($explodedSearchAnd as $needle) { + if ($needle[0] == '-' && strlen($needle) > 1) { + $excludeSearch[] = substr($needle, 1); + } else { + $andSearch[] = $needle; + } + } + + $keys = array('title', 'description', 'url', 'tags'); + + // Iterate over every stored link. + foreach ($this->links as $id => $link) { + // ignore non private links when 'privatonly' is on. + if ($visibility !== 'all') { + if (!$link['private'] && $visibility === 'private') { + continue; + } elseif ($link['private'] && $visibility === 'public') { + continue; + } + } + + // Concatenate link fields to search across fields. + // Adds a '\' separator for exact search terms. + $content = ''; + foreach ($keys as $key) { + $content .= mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8') . '\\'; + } + + // Be optimistic + $found = true; + + // First, we look for exact term search + for ($i = 0; $i < count($exactSearch) && $found; $i++) { + $found = strpos($content, $exactSearch[$i]) !== false; + } + + // Iterate over keywords, if keyword is not found, + // no need to check for the others. We want all or nothing. + for ($i = 0; $i < count($andSearch) && $found; $i++) { + $found = strpos($content, $andSearch[$i]) !== false; + } + + // Exclude terms. + for ($i = 0; $i < count($excludeSearch) && $found; $i++) { + $found = strpos($content, $excludeSearch[$i]) === false; + } + + if ($found) { + $filtered[$id] = $link; + } + } + + return $filtered; + } + + /** + * generate a regex fragment out of a tag + * + * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard + * + * @return string generated regex fragment + */ + private static function tag2regex($tag) + { + $len = strlen($tag); + if (!$len || $tag === "-" || $tag === "*") { + // nothing to search, return empty regex + return ''; + } + if ($tag[0] === "-") { + // query is negated + $i = 1; // use offset to start after '-' character + $regex = '(?!'; // create negative lookahead + } else { + $i = 0; // start at first character + $regex = '(?='; // use positive lookahead + } + $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning + // iterate over string, separating it into placeholder and content + for (; $i < $len; $i++) { + if ($tag[$i] === '*') { + // placeholder found + $regex .= '[^ ]*?'; + } else { + // regular characters + $offset = strpos($tag, '*', $i); + if ($offset === false) { + // no placeholder found, set offset to end of string + $offset = $len; + } + // subtract one, as we want to get before the placeholder or end of string + $offset -= 1; + // we got a tag name that we want to search for. escape any regex characters to prevent conflicts. + $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/'); + // move $i on + $i = $offset; + } + } + $regex .= '(?:$| ))'; // after the tag may only be a space or the end + return $regex; + } + + /** + * 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 string $visibility Optional: return only all/private/public links. + * + * @return array filtered links. + */ + public function filterTags($tags, $casesensitive = false, $visibility = 'all') + { + // get single tags (we may get passed an array, even though the docs say different) + $inputTags = $tags; + if (!is_array($tags)) { + // we got an input string, split tags + $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY); + } + + if (!count($inputTags)) { + // no input tags + return $this->noFilter($visibility); + } + + // build regex from all tags + $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/'; + if (!$casesensitive) { + // make regex case insensitive + $re .= 'i'; + } + + // create resulting array + $filtered = array(); + + // iterate over each link + foreach ($this->links as $key => $link) { + // check level of visibility + // ignore non private links when 'privateonly' is on. + if ($visibility !== 'all') { + if (!$link['private'] && $visibility === 'private') { + continue; + } elseif ($link['private'] && $visibility === 'public') { + continue; + } + } + $search = $link['tags']; // build search string, start with tags of current link + if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) { + // description given and at least one possible tag found + $descTags = array(); + // find all tags in the form of #tag in the description + preg_match_all( + '/(?links as $key => $link) { + if ($visibility !== 'all') { + if (!$link['private'] && $visibility === 'private') { + continue; + } elseif ($link['private'] && $visibility === 'public') { + continue; + } + } + + if (empty(trim($link['tags']))) { + $filtered[$key] = $link; + } + } + + 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 $key => $l) { + if ($l['created']->format('Ymd') == $day) { + $filtered[$key] = $l; + } + } + + // sort by date ASC + return array_reverse($filtered, true); + } + + /** + * Convert a list of tags (str) to an array. Also + * - handle case sensitivity. + * - accepts spaces commas as separator. + * + * @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 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 preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); + } +} diff --git a/application/bookmark/exception/LinkNotFoundException.php b/application/bookmark/exception/LinkNotFoundException.php new file mode 100644 index 00000000..f9414428 --- /dev/null +++ b/application/bookmark/exception/LinkNotFoundException.php @@ -0,0 +1,15 @@ +message = t('The link you are trying to reach does not exist or has been deleted.'); + } +} -- cgit v1.2.3 From fe3713d2e5c91e2d07af72b39f321521d3dd470c Mon Sep 17 00:00:00 2001 From: VirtualTam Date: Mon, 3 Dec 2018 01:35:14 +0100 Subject: namespacing: move LinkUtils along \Shaarli\Bookmark classes Signed-off-by: VirtualTam --- application/LinkUtils.php | 222 ------------------------------------- application/bookmark/LinkUtils.php | 222 +++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 222 deletions(-) delete mode 100644 application/LinkUtils.php create mode 100644 application/bookmark/LinkUtils.php (limited to 'application') diff --git a/application/LinkUtils.php b/application/LinkUtils.php deleted file mode 100644 index b5110edc..00000000 --- a/application/LinkUtils.php +++ /dev/null @@ -1,222 +0,0 @@ -(.*?)!is', $html, $matches)) { - return trim(str_replace("\n", '', $matches[1])); - } - return false; -} - -/** - * Extract charset from HTTP header if it's defined. - * - * @param string $header HTTP header Content-Type line. - * - * @return bool|string Charset string if found (lowercase), false otherwise. - */ -function header_extract_charset($header) -{ - preg_match('/charset="?([^; ]+)/i', $header, $match); - if (! empty($match[1])) { - return strtolower(trim($match[1])); - } - - return false; -} - -/** - * Extract charset HTML content (tag ). - * - * @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('#/]+)["\']? */?>#Usi', $html, $enc); - if (!empty($enc[1])) { - return strtolower($enc[1]); - } - - return false; -} - -/** - * Count private links in given linklist. - * - * @param array|Countable $links Linklist. - * - * @return int Number of private links. - */ -function count_private($links) -{ - $cpt = 0; - foreach ($links as $link) { - if ($link['private']) { - $cpt += 1; - } - } - - return $cpt; -} - -/** - * In a string, converts URLs to clickable links. - * - * @param string $text input string. - * @param string $redirector if a redirector is set, use it to gerenate links. - * @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not. - * - * @return string returns $text with all links converted to HTML links. - * - * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 - */ -function text2clickable($text, $redirector = '', $urlEncode = true) -{ - $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si'; - - if (empty($redirector)) { - return preg_replace($regex, '$1', $text); - } - // Redirector is set, urlencode the final URL. - return preg_replace_callback( - $regex, - function ($matches) use ($redirector, $urlEncode) { - $url = $urlEncode ? urlencode($matches[1]) : $matches[1]; - return ''. $matches[1] .''; - }, - $text - ); -} - -/** - * Auto-link hashtags. - * - * @param string $description Given description. - * @param string $indexUrl Root URL. - * - * @return string Description with auto-linked hashtags. - */ -function hashtag_autolink($description, $indexUrl = '') -{ - /* - * To support unicode: http://stackoverflow.com/a/35498078/1484919 - * \p{Pc} - to match underscore - * \p{N} - numeric character in any script - * \p{L} - letter from any language - * \p{Mn} - any non marking space (accents, umlauts, etc) - */ - $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; - $replacement = '$1#$2'; - return preg_replace($regex, $replacement, $description); -} - -/** - * This function inserts   where relevant so that multiple spaces are properly displayed in HTML - * even in the absence of
  (This is used in description to keep text formatting).
- *
- * @param string $text input text.
- *
- * @return string formatted text.
- */
-function space2nbsp($text)
-{
-    return preg_replace('/(^| ) /m', '$1 ', $text);
-}
-
-/**
- * Format Shaarli's description
- *
- * @param string $description shaare's description.
- * @param string $redirector  if a redirector is set, use it to gerenate links.
- * @param bool   $urlEncode  Use `urlencode()` on the URL after the redirector or not.
- * @param string $indexUrl    URL to Shaarli's index.
-
- * @return string formatted description.
- */
-function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '')
-{
-    return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
-}
-
-/**
- * Generate a small hash for a link.
- *
- * @param DateTime $date Link creation date.
- * @param int      $id   Link ID.
- *
- * @return string the small hash generated from link data.
- */
-function link_small_hash($date, $id)
-{
-    return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id);
-}
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
new file mode 100644
index 00000000..de5b61cb
--- /dev/null
+++ b/application/bookmark/LinkUtils.php
@@ -0,0 +1,222 @@
+(.*?)!is', $html, $matches)) {
+        return trim(str_replace("\n", '', $matches[1]));
+    }
+    return false;
+}
+
+/**
+ * Extract charset from HTTP header if it's defined.
+ *
+ * @param string $header HTTP header Content-Type line.
+ *
+ * @return bool|string Charset string if found (lowercase), false otherwise.
+ */
+function header_extract_charset($header)
+{
+    preg_match('/charset="?([^; ]+)/i', $header, $match);
+    if (! empty($match[1])) {
+        return strtolower(trim($match[1]));
+    }
+
+    return false;
+}
+
+/**
+ * Extract charset HTML content (tag ).
+ *
+ * @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('#/]+)["\']? */?>#Usi', $html, $enc);
+    if (!empty($enc[1])) {
+        return strtolower($enc[1]);
+    }
+
+    return false;
+}
+
+/**
+ * Count private links in given linklist.
+ *
+ * @param array|Countable $links Linklist.
+ *
+ * @return int Number of private links.
+ */
+function count_private($links)
+{
+    $cpt = 0;
+    foreach ($links as $link) {
+        if ($link['private']) {
+            $cpt += 1;
+        }
+    }
+
+    return $cpt;
+}
+
+/**
+ * In a string, converts URLs to clickable links.
+ *
+ * @param string $text       input string.
+ * @param string $redirector if a redirector is set, use it to gerenate links.
+ * @param bool   $urlEncode  Use `urlencode()` on the URL after the redirector or not.
+ *
+ * @return string returns $text with all links converted to HTML links.
+ *
+ * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
+ */
+function text2clickable($text, $redirector = '', $urlEncode = true)
+{
+    $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
+
+    if (empty($redirector)) {
+        return preg_replace($regex, '$1', $text);
+    }
+    // Redirector is set, urlencode the final URL.
+    return preg_replace_callback(
+        $regex,
+        function ($matches) use ($redirector, $urlEncode) {
+            $url = $urlEncode ? urlencode($matches[1]) : $matches[1];
+            return ''. $matches[1] .'';
+        },
+        $text
+    );
+}
+
+/**
+ * Auto-link hashtags.
+ *
+ * @param string $description Given description.
+ * @param string $indexUrl    Root URL.
+ *
+ * @return string Description with auto-linked hashtags.
+ */
+function hashtag_autolink($description, $indexUrl = '')
+{
+    /*
+     * To support unicode: http://stackoverflow.com/a/35498078/1484919
+     * \p{Pc} - to match underscore
+     * \p{N} - numeric character in any script
+     * \p{L} - letter from any language
+     * \p{Mn} - any non marking space (accents, umlauts, etc)
+     */
+    $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
+    $replacement = '$1#$2';
+    return preg_replace($regex, $replacement, $description);
+}
+
+/**
+ * This function inserts   where relevant so that multiple spaces are properly displayed in HTML
+ * even in the absence of 
  (This is used in description to keep text formatting).
+ *
+ * @param string $text input text.
+ *
+ * @return string formatted text.
+ */
+function space2nbsp($text)
+{
+    return preg_replace('/(^| ) /m', '$1 ', $text);
+}
+
+/**
+ * Format Shaarli's description
+ *
+ * @param string $description shaare's description.
+ * @param string $redirector  if a redirector is set, use it to gerenate links.
+ * @param bool   $urlEncode   Use `urlencode()` on the URL after the redirector or not.
+ * @param string $indexUrl    URL to Shaarli's index.
+
+ * @return string formatted description.
+ */
+function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '')
+{
+    return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
+}
+
+/**
+ * Generate a small hash for a link.
+ *
+ * @param DateTime $date Link creation date.
+ * @param int      $id   Link ID.
+ *
+ * @return string the small hash generated from link data.
+ */
+function link_small_hash($date, $id)
+{
+    return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id);
+}
-- 
cgit v1.2.3


From bcf056c9d92e5240e645c76a4cdc8ae159693f9a Mon Sep 17 00:00:00 2001
From: VirtualTam 
Date: Mon, 3 Dec 2018 23:49:20 +0100
Subject: namespacing: \Shaarli\Updater

Signed-off-by: VirtualTam 
---
 application/Updater.php                            | 637 ---------------------
 application/config/ConfigPhp.php                   |   2 +-
 application/updater/Updater.php                    | 553 ++++++++++++++++++
 application/updater/UpdaterUtils.php               |  39 ++
 application/updater/exception/UpdaterException.php |  60 ++
 5 files changed, 653 insertions(+), 638 deletions(-)
 delete mode 100644 application/Updater.php
 create mode 100644 application/updater/Updater.php
 create mode 100644 application/updater/UpdaterUtils.php
 create mode 100644 application/updater/exception/UpdaterException.php

(limited to 'application')

diff --git a/application/Updater.php b/application/Updater.php
deleted file mode 100644
index ca05ecc2..00000000
--- a/application/Updater.php
+++ /dev/null
@@ -1,637 +0,0 @@
-doneUpdates = $doneUpdates;
-        $this->linkDB = $linkDB;
-        $this->conf = $conf;
-        $this->isLoggedIn = $isLoggedIn;
-        $this->session = &$session;
-
-        // Retrieve all update methods.
-        $class = new ReflectionClass($this);
-        $this->methods = $class->getMethods();
-    }
-
-    /**
-     * Run all new updates.
-     * Update methods have to start with 'updateMethod' and return true (on success).
-     *
-     * @return array An array containing ran updates.
-     *
-     * @throws UpdaterException If something went wrong.
-     */
-    public function update()
-    {
-        $updatesRan = array();
-
-        // If the user isn't logged in, exit without updating.
-        if ($this->isLoggedIn !== true) {
-            return $updatesRan;
-        }
-
-        if ($this->methods === null) {
-            throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
-        }
-
-        foreach ($this->methods as $method) {
-            // Not an update method or already done, pass.
-            if (! startsWith($method->getName(), 'updateMethod')
-                || in_array($method->getName(), $this->doneUpdates)
-            ) {
-                continue;
-            }
-
-            try {
-                $method->setAccessible(true);
-                $res = $method->invoke($this);
-                // Update method must return true to be considered processed.
-                if ($res === true) {
-                    $updatesRan[] = $method->getName();
-                }
-            } catch (Exception $e) {
-                throw new UpdaterException($method, $e);
-            }
-        }
-
-        $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
-
-        return $updatesRan;
-    }
-
-    /**
-     * @return array Updates methods already processed.
-     */
-    public function getDoneUpdates()
-    {
-        return $this->doneUpdates;
-    }
-
-    /**
-     * Move deprecated options.php to config.php.
-     *
-     * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
-     *    options.php is not supported anymore.
-     */
-    public function updateMethodMergeDeprecatedConfigFile()
-    {
-        if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
-            include $this->conf->get('resource.data_dir') . '/options.php';
-
-            // Load GLOBALS into config
-            $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
-            $allowedKeys[] = 'config';
-            foreach ($GLOBALS as $key => $value) {
-                if (in_array($key, $allowedKeys)) {
-                    $this->conf->set($key, $value);
-                }
-            }
-            $this->conf->write($this->isLoggedIn);
-            unlink($this->conf->get('resource.data_dir').'/options.php');
-        }
-
-        return true;
-    }
-
-    /**
-     * Move old configuration in PHP to the new config system in JSON format.
-     *
-     * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
-     * It will also convert legacy setting keys to the new ones.
-     */
-    public function updateMethodConfigToJson()
-    {
-        // JSON config already exists, nothing to do.
-        if ($this->conf->getConfigIO() instanceof ConfigJson) {
-            return true;
-        }
-
-        $configPhp = new ConfigPhp();
-        $configJson = new ConfigJson();
-        $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
-        rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
-        $this->conf->setConfigIO($configJson);
-        $this->conf->reload();
-
-        $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
-        foreach (ConfigPhp::$ROOT_KEYS as $key) {
-            $this->conf->set($legacyMap[$key], $oldConfig[$key]);
-        }
-
-        // Set sub config keys (config and plugins)
-        $subConfig = array('config', 'plugins');
-        foreach ($subConfig as $sub) {
-            foreach ($oldConfig[$sub] as $key => $value) {
-                if (isset($legacyMap[$sub .'.'. $key])) {
-                    $configKey = $legacyMap[$sub .'.'. $key];
-                } else {
-                    $configKey = $sub .'.'. $key;
-                }
-                $this->conf->set($configKey, $value);
-            }
-        }
-
-        try {
-            $this->conf->write($this->isLoggedIn);
-            return true;
-        } catch (IOException $e) {
-            error_log($e->getMessage());
-            return false;
-        }
-    }
-
-    /**
-     * Escape settings which have been manually escaped in every request in previous versions:
-     *   - general.title
-     *   - general.header_link
-     *   - redirector.url
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodEscapeUnescapedConfig()
-    {
-        try {
-            $this->conf->set('general.title', escape($this->conf->get('general.title')));
-            $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
-            $this->conf->set('redirector.url', escape($this->conf->get('redirector.url')));
-            $this->conf->write($this->isLoggedIn);
-        } catch (Exception $e) {
-            error_log($e->getMessage());
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Update the database to use the new ID system, which replaces linkdate primary keys.
-     * Also, creation and update dates are now DateTime objects (done by LinkDB).
-     *
-     * Since this update is very sensitve (changing the whole database), the datastore will be
-     * automatically backed up into the file datastore..php.
-     *
-     * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
-     * which will be saved by this method.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodDatastoreIds()
-    {
-        // up to date database
-        if (isset($this->linkDB[0])) {
-            return true;
-        }
-
-        $save = $this->conf->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php';
-        copy($this->conf->get('resource.datastore'), $save);
-
-        $links = array();
-        foreach ($this->linkDB as $offset => $value) {
-            $links[] = $value;
-            unset($this->linkDB[$offset]);
-        }
-        $links = array_reverse($links);
-        $cpt = 0;
-        foreach ($links as $l) {
-            unset($l['linkdate']);
-            $l['id'] = $cpt;
-            $this->linkDB[$cpt++] = $l;
-        }
-
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-        $this->linkDB->reorder();
-
-        return true;
-    }
-
-    /**
-     * Rename tags starting with a '-' to work with tag exclusion search.
-     */
-    public function updateMethodRenameDashTags()
-    {
-        $linklist = $this->linkDB->filterSearch();
-        foreach ($linklist as $key => $link) {
-            $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
-            $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
-            $this->linkDB[$key] = $link;
-        }
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-        return true;
-    }
-
-    /**
-     * Initialize API settings:
-     *   - api.enabled: true
-     *   - api.secret: generated secret
-     */
-    public function updateMethodApiSettings()
-    {
-        if ($this->conf->exists('api.secret')) {
-            return true;
-        }
-
-        $this->conf->set('api.enabled', true);
-        $this->conf->set(
-            'api.secret',
-            generate_api_secret(
-                $this->conf->get('credentials.login'),
-                $this->conf->get('credentials.salt')
-            )
-        );
-        $this->conf->write($this->isLoggedIn);
-        return true;
-    }
-
-    /**
-     * New setting: theme name. If the default theme is used, nothing to do.
-     *
-     * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
-     * and the current theme is set as default in the theme setting.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodDefaultTheme()
-    {
-        // raintpl_tpl isn't the root template directory anymore.
-        // We run the update only if this folder still contains the template files.
-        $tplDir = $this->conf->get('resource.raintpl_tpl');
-        $tplFile = $tplDir . '/linklist.html';
-        if (! file_exists($tplFile)) {
-            return true;
-        }
-
-        $parent = dirname($tplDir);
-        $this->conf->set('resource.raintpl_tpl', $parent);
-        $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
-        $this->conf->write($this->isLoggedIn);
-
-        // Dependency injection gore
-        RainTPL::$tpl_dir = $tplDir;
-
-        return true;
-    }
-
-    /**
-     * Move the file to inc/user.css to data/user.css.
-     *
-     * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodMoveUserCss()
-    {
-        if (! is_file('inc/user.css')) {
-            return true;
-        }
-
-        return rename('inc/user.css', 'data/user.css');
-    }
-
-    /**
-     * * `markdown_escape` is a new setting, set to true as default.
-     *
-     * If the markdown plugin was already enabled, escaping is disabled to avoid
-     * breaking existing entries.
-     */
-    public function updateMethodEscapeMarkdown()
-    {
-        if ($this->conf->exists('security.markdown_escape')) {
-            return true;
-        }
-
-        if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
-            $this->conf->set('security.markdown_escape', false);
-        } else {
-            $this->conf->set('security.markdown_escape', true);
-        }
-        $this->conf->write($this->isLoggedIn);
-
-        return true;
-    }
-
-    /**
-     * Add 'http://' to Piwik URL the setting is set.
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodPiwikUrl()
-    {
-        if (! $this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
-            return true;
-        }
-
-        $this->conf->set('plugins.PIWIK_URL', 'http://'. $this->conf->get('plugins.PIWIK_URL'));
-        $this->conf->write($this->isLoggedIn);
-
-        return true;
-    }
-
-    /**
-     * Use ATOM feed as default.
-     */
-    public function updateMethodAtomDefault()
-    {
-        if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
-            return true;
-        }
-
-        $this->conf->set('feed.show_atom', true);
-        $this->conf->write($this->isLoggedIn);
-
-        return true;
-    }
-
-    /**
-     * Update updates.check_updates_branch setting.
-     *
-     * If the current major version digit matches the latest branch
-     * major version digit, we set the branch to `latest`,
-     * otherwise we'll check updates on the `stable` branch.
-     *
-     * No update required for the dev version.
-     *
-     * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
-     *
-     * FIXME! This needs to be removed when we switch to first digit major version
-     *        instead of the second one since the versionning process will change.
-     */
-    public function updateMethodCheckUpdateRemoteBranch()
-    {
-        if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
-            return true;
-        }
-
-        // Get latest branch major version digit
-        $latestVersion = ApplicationUtils::getLatestGitVersionCode(
-            'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
-            5
-        );
-        if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
-            return false;
-        }
-        $latestMajor = $matches[1];
-
-        // Get current major version digit
-        preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
-        $currentMajor = $matches[1];
-
-        if ($currentMajor === $latestMajor) {
-            $branch = 'latest';
-        } else {
-            $branch = 'stable';
-        }
-        $this->conf->set('updates.check_updates_branch', $branch);
-        $this->conf->write($this->isLoggedIn);
-        return true;
-    }
-
-    /**
-     * Reset history store file due to date format change.
-     */
-    public function updateMethodResetHistoryFile()
-    {
-        if (is_file($this->conf->get('resource.history'))) {
-            unlink($this->conf->get('resource.history'));
-        }
-        return true;
-    }
-
-    /**
-     * Save the datastore -> the link order is now applied when links are saved.
-     */
-    public function updateMethodReorderDatastore()
-    {
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-        return true;
-    }
-
-    /**
-     * Change privateonly session key to visibility.
-     */
-    public function updateMethodVisibilitySession()
-    {
-        if (isset($_SESSION['privateonly'])) {
-            unset($_SESSION['privateonly']);
-            $_SESSION['visibility'] = 'private';
-        }
-        return true;
-    }
-
-    /**
-     * Add download size and timeout to the configuration file
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodDownloadSizeAndTimeoutConf()
-    {
-        if ($this->conf->exists('general.download_max_size')
-            && $this->conf->exists('general.download_timeout')
-        ) {
-            return true;
-        }
-
-        if (! $this->conf->exists('general.download_max_size')) {
-            $this->conf->set('general.download_max_size', 1024*1024*4);
-        }
-
-        if (! $this->conf->exists('general.download_timeout')) {
-            $this->conf->set('general.download_timeout', 30);
-        }
-
-        $this->conf->write($this->isLoggedIn);
-        return true;
-    }
-
-    /**
-     * * Move thumbnails management to WebThumbnailer, coming with new settings.
-     */
-    public function updateMethodWebThumbnailer()
-    {
-        if ($this->conf->exists('thumbnails.mode')) {
-            return true;
-        }
-
-        $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
-        $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
-        $this->conf->set('thumbnails.width', 125);
-        $this->conf->set('thumbnails.height', 90);
-        $this->conf->remove('thumbnail');
-        $this->conf->write(true);
-
-        if ($thumbnailsEnabled) {
-            $this->session['warnings'][] = t(
-                'You have enabled or changed thumbnails mode. Please synchronize them.'
-            );
-        }
-
-        return true;
-    }
-
-    /**
-     * Set sticky = false on all links
-     *
-     * @return bool true if the update is successful, false otherwise.
-     */
-    public function updateMethodSetSticky()
-    {
-        foreach ($this->linkDB as $key => $link) {
-            if (isset($link['sticky'])) {
-                return true;
-            }
-            $link['sticky'] = false;
-            $this->linkDB[$key] = $link;
-        }
-
-        $this->linkDB->save($this->conf->get('resource.page_cache'));
-
-        return true;
-    }
-}
-
-/**
- * Class UpdaterException.
- */
-class UpdaterException extends Exception
-{
-    /**
-     * @var string Method where the error occurred.
-     */
-    protected $method;
-
-    /**
-     * @var Exception The parent exception.
-     */
-    protected $previous;
-
-    /**
-     * Constructor.
-     *
-     * @param string         $message  Force the error message if set.
-     * @param string         $method   Method where the error occurred.
-     * @param Exception|bool $previous Parent exception.
-     */
-    public function __construct($message = '', $method = '', $previous = false)
-    {
-        $this->method = $method;
-        $this->previous = $previous;
-        $this->message = $this->buildMessage($message);
-    }
-
-    /**
-     * Build the exception error message.
-     *
-     * @param string $message Optional given error message.
-     *
-     * @return string The built error message.
-     */
-    private function buildMessage($message)
-    {
-        $out = '';
-        if (! empty($message)) {
-            $out .= $message . PHP_EOL;
-        }
-
-        if (! empty($this->method)) {
-            $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
-        }
-
-        if (! empty($this->previous)) {
-            $out .= '  '. $this->previous->getMessage();
-        }
-
-        return $out;
-    }
-}
-
-/**
- * Read the updates file, and return already done updates.
- *
- * @param string $updatesFilepath Updates file path.
- *
- * @return array Already done update methods.
- */
-function read_updates_file($updatesFilepath)
-{
-    if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
-        $content = file_get_contents($updatesFilepath);
-        if (! empty($content)) {
-            return explode(';', $content);
-        }
-    }
-    return array();
-}
-
-/**
- * Write updates file.
- *
- * @param string $updatesFilepath Updates file path.
- * @param array  $updates         Updates array to write.
- *
- * @throws Exception Couldn't write version number.
- */
-function write_updates_file($updatesFilepath, $updates)
-{
-    if (empty($updatesFilepath)) {
-        throw new Exception(t('Updates file path is not set, can\'t write updates.'));
-    }
-
-    $res = file_put_contents($updatesFilepath, implode(';', $updates));
-    if ($res === false) {
-        throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
-    }
-}
diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php
index 9ed5d31f..cad34594 100644
--- a/application/config/ConfigPhp.php
+++ b/application/config/ConfigPhp.php
@@ -27,7 +27,7 @@ class ConfigPhp implements ConfigIO
     /**
      * Map legacy config keys with the new ones.
      * If ConfigPhp is used, getting  will actually look for .
-     * The Updater will use this array to transform keys when switching to JSON.
+     * The updater will use this array to transform keys when switching to JSON.
      *
      * @var array current key => legacy key.
      */
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
new file mode 100644
index 00000000..55251a30
--- /dev/null
+++ b/application/updater/Updater.php
@@ -0,0 +1,553 @@
+doneUpdates = $doneUpdates;
+        $this->linkDB = $linkDB;
+        $this->conf = $conf;
+        $this->isLoggedIn = $isLoggedIn;
+        $this->session = &$session;
+
+        // Retrieve all update methods.
+        $class = new ReflectionClass($this);
+        $this->methods = $class->getMethods();
+    }
+
+    /**
+     * Run all new updates.
+     * Update methods have to start with 'updateMethod' and return true (on success).
+     *
+     * @return array An array containing ran updates.
+     *
+     * @throws UpdaterException If something went wrong.
+     */
+    public function update()
+    {
+        $updatesRan = array();
+
+        // If the user isn't logged in, exit without updating.
+        if ($this->isLoggedIn !== true) {
+            return $updatesRan;
+        }
+
+        if ($this->methods === null) {
+            throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
+        }
+
+        foreach ($this->methods as $method) {
+            // Not an update method or already done, pass.
+            if (!startsWith($method->getName(), 'updateMethod')
+                || in_array($method->getName(), $this->doneUpdates)
+            ) {
+                continue;
+            }
+
+            try {
+                $method->setAccessible(true);
+                $res = $method->invoke($this);
+                // Update method must return true to be considered processed.
+                if ($res === true) {
+                    $updatesRan[] = $method->getName();
+                }
+            } catch (Exception $e) {
+                throw new UpdaterException($method, $e);
+            }
+        }
+
+        $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
+
+        return $updatesRan;
+    }
+
+    /**
+     * @return array Updates methods already processed.
+     */
+    public function getDoneUpdates()
+    {
+        return $this->doneUpdates;
+    }
+
+    /**
+     * Move deprecated options.php to config.php.
+     *
+     * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
+     *    options.php is not supported anymore.
+     */
+    public function updateMethodMergeDeprecatedConfigFile()
+    {
+        if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
+            include $this->conf->get('resource.data_dir') . '/options.php';
+
+            // Load GLOBALS into config
+            $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
+            $allowedKeys[] = 'config';
+            foreach ($GLOBALS as $key => $value) {
+                if (in_array($key, $allowedKeys)) {
+                    $this->conf->set($key, $value);
+                }
+            }
+            $this->conf->write($this->isLoggedIn);
+            unlink($this->conf->get('resource.data_dir') . '/options.php');
+        }
+
+        return true;
+    }
+
+    /**
+     * Move old configuration in PHP to the new config system in JSON format.
+     *
+     * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
+     * It will also convert legacy setting keys to the new ones.
+     */
+    public function updateMethodConfigToJson()
+    {
+        // JSON config already exists, nothing to do.
+        if ($this->conf->getConfigIO() instanceof ConfigJson) {
+            return true;
+        }
+
+        $configPhp = new ConfigPhp();
+        $configJson = new ConfigJson();
+        $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
+        rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
+        $this->conf->setConfigIO($configJson);
+        $this->conf->reload();
+
+        $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
+        foreach (ConfigPhp::$ROOT_KEYS as $key) {
+            $this->conf->set($legacyMap[$key], $oldConfig[$key]);
+        }
+
+        // Set sub config keys (config and plugins)
+        $subConfig = array('config', 'plugins');
+        foreach ($subConfig as $sub) {
+            foreach ($oldConfig[$sub] as $key => $value) {
+                if (isset($legacyMap[$sub . '.' . $key])) {
+                    $configKey = $legacyMap[$sub . '.' . $key];
+                } else {
+                    $configKey = $sub . '.' . $key;
+                }
+                $this->conf->set($configKey, $value);
+            }
+        }
+
+        try {
+            $this->conf->write($this->isLoggedIn);
+            return true;
+        } catch (IOException $e) {
+            error_log($e->getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Escape settings which have been manually escaped in every request in previous versions:
+     *   - general.title
+     *   - general.header_link
+     *   - redirector.url
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodEscapeUnescapedConfig()
+    {
+        try {
+            $this->conf->set('general.title', escape($this->conf->get('general.title')));
+            $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
+            $this->conf->set('redirector.url', escape($this->conf->get('redirector.url')));
+            $this->conf->write($this->isLoggedIn);
+        } catch (Exception $e) {
+            error_log($e->getMessage());
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Update the database to use the new ID system, which replaces linkdate primary keys.
+     * Also, creation and update dates are now DateTime objects (done by LinkDB).
+     *
+     * Since this update is very sensitve (changing the whole database), the datastore will be
+     * automatically backed up into the file datastore..php.
+     *
+     * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
+     * which will be saved by this method.
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodDatastoreIds()
+    {
+        // up to date database
+        if (isset($this->linkDB[0])) {
+            return true;
+        }
+
+        $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
+        copy($this->conf->get('resource.datastore'), $save);
+
+        $links = array();
+        foreach ($this->linkDB as $offset => $value) {
+            $links[] = $value;
+            unset($this->linkDB[$offset]);
+        }
+        $links = array_reverse($links);
+        $cpt = 0;
+        foreach ($links as $l) {
+            unset($l['linkdate']);
+            $l['id'] = $cpt;
+            $this->linkDB[$cpt++] = $l;
+        }
+
+        $this->linkDB->save($this->conf->get('resource.page_cache'));
+        $this->linkDB->reorder();
+
+        return true;
+    }
+
+    /**
+     * Rename tags starting with a '-' to work with tag exclusion search.
+     */
+    public function updateMethodRenameDashTags()
+    {
+        $linklist = $this->linkDB->filterSearch();
+        foreach ($linklist as $key => $link) {
+            $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
+            $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
+            $this->linkDB[$key] = $link;
+        }
+        $this->linkDB->save($this->conf->get('resource.page_cache'));
+        return true;
+    }
+
+    /**
+     * Initialize API settings:
+     *   - api.enabled: true
+     *   - api.secret: generated secret
+     */
+    public function updateMethodApiSettings()
+    {
+        if ($this->conf->exists('api.secret')) {
+            return true;
+        }
+
+        $this->conf->set('api.enabled', true);
+        $this->conf->set(
+            'api.secret',
+            generate_api_secret(
+                $this->conf->get('credentials.login'),
+                $this->conf->get('credentials.salt')
+            )
+        );
+        $this->conf->write($this->isLoggedIn);
+        return true;
+    }
+
+    /**
+     * New setting: theme name. If the default theme is used, nothing to do.
+     *
+     * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
+     * and the current theme is set as default in the theme setting.
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodDefaultTheme()
+    {
+        // raintpl_tpl isn't the root template directory anymore.
+        // We run the update only if this folder still contains the template files.
+        $tplDir = $this->conf->get('resource.raintpl_tpl');
+        $tplFile = $tplDir . '/linklist.html';
+        if (!file_exists($tplFile)) {
+            return true;
+        }
+
+        $parent = dirname($tplDir);
+        $this->conf->set('resource.raintpl_tpl', $parent);
+        $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
+        $this->conf->write($this->isLoggedIn);
+
+        // Dependency injection gore
+        RainTPL::$tpl_dir = $tplDir;
+
+        return true;
+    }
+
+    /**
+     * Move the file to inc/user.css to data/user.css.
+     *
+     * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodMoveUserCss()
+    {
+        if (!is_file('inc/user.css')) {
+            return true;
+        }
+
+        return rename('inc/user.css', 'data/user.css');
+    }
+
+    /**
+     * * `markdown_escape` is a new setting, set to true as default.
+     *
+     * If the markdown plugin was already enabled, escaping is disabled to avoid
+     * breaking existing entries.
+     */
+    public function updateMethodEscapeMarkdown()
+    {
+        if ($this->conf->exists('security.markdown_escape')) {
+            return true;
+        }
+
+        if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
+            $this->conf->set('security.markdown_escape', false);
+        } else {
+            $this->conf->set('security.markdown_escape', true);
+        }
+        $this->conf->write($this->isLoggedIn);
+
+        return true;
+    }
+
+    /**
+     * Add 'http://' to Piwik URL the setting is set.
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodPiwikUrl()
+    {
+        if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
+            return true;
+        }
+
+        $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
+        $this->conf->write($this->isLoggedIn);
+
+        return true;
+    }
+
+    /**
+     * Use ATOM feed as default.
+     */
+    public function updateMethodAtomDefault()
+    {
+        if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
+            return true;
+        }
+
+        $this->conf->set('feed.show_atom', true);
+        $this->conf->write($this->isLoggedIn);
+
+        return true;
+    }
+
+    /**
+     * Update updates.check_updates_branch setting.
+     *
+     * If the current major version digit matches the latest branch
+     * major version digit, we set the branch to `latest`,
+     * otherwise we'll check updates on the `stable` branch.
+     *
+     * No update required for the dev version.
+     *
+     * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
+     *
+     * FIXME! This needs to be removed when we switch to first digit major version
+     *        instead of the second one since the versionning process will change.
+     */
+    public function updateMethodCheckUpdateRemoteBranch()
+    {
+        if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
+            return true;
+        }
+
+        // Get latest branch major version digit
+        $latestVersion = ApplicationUtils::getLatestGitVersionCode(
+            'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
+            5
+        );
+        if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
+            return false;
+        }
+        $latestMajor = $matches[1];
+
+        // Get current major version digit
+        preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
+        $currentMajor = $matches[1];
+
+        if ($currentMajor === $latestMajor) {
+            $branch = 'latest';
+        } else {
+            $branch = 'stable';
+        }
+        $this->conf->set('updates.check_updates_branch', $branch);
+        $this->conf->write($this->isLoggedIn);
+        return true;
+    }
+
+    /**
+     * Reset history store file due to date format change.
+     */
+    public function updateMethodResetHistoryFile()
+    {
+        if (is_file($this->conf->get('resource.history'))) {
+            unlink($this->conf->get('resource.history'));
+        }
+        return true;
+    }
+
+    /**
+     * Save the datastore -> the link order is now applied when links are saved.
+     */
+    public function updateMethodReorderDatastore()
+    {
+        $this->linkDB->save($this->conf->get('resource.page_cache'));
+        return true;
+    }
+
+    /**
+     * Change privateonly session key to visibility.
+     */
+    public function updateMethodVisibilitySession()
+    {
+        if (isset($_SESSION['privateonly'])) {
+            unset($_SESSION['privateonly']);
+            $_SESSION['visibility'] = 'private';
+        }
+        return true;
+    }
+
+    /**
+     * Add download size and timeout to the configuration file
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodDownloadSizeAndTimeoutConf()
+    {
+        if ($this->conf->exists('general.download_max_size')
+            && $this->conf->exists('general.download_timeout')
+        ) {
+            return true;
+        }
+
+        if (!$this->conf->exists('general.download_max_size')) {
+            $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
+        }
+
+        if (!$this->conf->exists('general.download_timeout')) {
+            $this->conf->set('general.download_timeout', 30);
+        }
+
+        $this->conf->write($this->isLoggedIn);
+        return true;
+    }
+
+    /**
+     * * Move thumbnails management to WebThumbnailer, coming with new settings.
+     */
+    public function updateMethodWebThumbnailer()
+    {
+        if ($this->conf->exists('thumbnails.mode')) {
+            return true;
+        }
+
+        $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
+        $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
+        $this->conf->set('thumbnails.width', 125);
+        $this->conf->set('thumbnails.height', 90);
+        $this->conf->remove('thumbnail');
+        $this->conf->write(true);
+
+        if ($thumbnailsEnabled) {
+            $this->session['warnings'][] = t(
+                'You have enabled or changed thumbnails mode. Please synchronize them.'
+            );
+        }
+
+        return true;
+    }
+
+    /**
+     * Set sticky = false on all links
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodSetSticky()
+    {
+        foreach ($this->linkDB as $key => $link) {
+            if (isset($link['sticky'])) {
+                return true;
+            }
+            $link['sticky'] = false;
+            $this->linkDB[$key] = $link;
+        }
+
+        $this->linkDB->save($this->conf->get('resource.page_cache'));
+
+        return true;
+    }
+}
diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php
new file mode 100644
index 00000000..34d4f422
--- /dev/null
+++ b/application/updater/UpdaterUtils.php
@@ -0,0 +1,39 @@
+method = $method;
+        $this->previous = $previous;
+        $this->message = $this->buildMessage($message);
+    }
+
+    /**
+     * Build the exception error message.
+     *
+     * @param string $message Optional given error message.
+     *
+     * @return string The built error message.
+     */
+    private function buildMessage($message)
+    {
+        $out = '';
+        if (!empty($message)) {
+            $out .= $message . PHP_EOL;
+        }
+
+        if (!empty($this->method)) {
+            $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
+        }
+
+        if (!empty($this->previous)) {
+            $out .= '  ' . $this->previous->getMessage();
+        }
+
+        return $out;
+    }
+}
-- 
cgit v1.2.3


From 9778a1551ce708b9f421a181806412a05410f1fb Mon Sep 17 00:00:00 2001
From: VirtualTam 
Date: Mon, 3 Dec 2018 23:58:59 +0100
Subject: namespacing: \Shaarli\ApplicationUtils

Signed-off-by: VirtualTam 
---
 application/ApplicationUtils.php   | 75 ++++++++++++++++++++------------------
 application/render/PageBuilder.php |  2 +-
 application/updater/Updater.php    |  2 +-
 3 files changed, 42 insertions(+), 37 deletions(-)

(limited to 'application')

diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
index a3b2dcb1..7fe3cb32 100644
--- a/application/ApplicationUtils.php
+++ b/application/ApplicationUtils.php
@@ -1,4 +1,9 @@
 get('resource.theme'),
-        ) as $path) {
-            if (! is_readable(realpath($path))) {
-                $errors[] = '"'.$path.'" '. t('directory is not readable');
+                     'application',
+                     'inc',
+                     'plugins',
+                     $rainTplDir,
+                     $rainTplDir . '/' . $conf->get('resource.theme'),
+                 ) as $path) {
+            if (!is_readable(realpath($path))) {
+                $errors[] = '"' . $path . '" ' . t('directory is not readable');
             }
         }
 
         // Check cache and data directories are readable and writable
         foreach (array(
-            $conf->get('resource.thumbnails_cache'),
-            $conf->get('resource.data_dir'),
-            $conf->get('resource.page_cache'),
-            $conf->get('resource.raintpl_tmp'),
-        ) as $path) {
-            if (! is_readable(realpath($path))) {
-                $errors[] = '"'.$path.'" '. t('directory is not readable');
+                     $conf->get('resource.thumbnails_cache'),
+                     $conf->get('resource.data_dir'),
+                     $conf->get('resource.page_cache'),
+                     $conf->get('resource.raintpl_tmp'),
+                 ) as $path) {
+            if (!is_readable(realpath($path))) {
+                $errors[] = '"' . $path . '" ' . t('directory is not readable');
             }
-            if (! is_writable(realpath($path))) {
-                $errors[] = '"'.$path.'" '. t('directory is not writable');
+            if (!is_writable(realpath($path))) {
+                $errors[] = '"' . $path . '" ' . t('directory is not writable');
             }
         }
 
         // Check configuration files are readable and writable
         foreach (array(
-            $conf->getConfigFileExt(),
-            $conf->get('resource.datastore'),
-            $conf->get('resource.ban_file'),
-            $conf->get('resource.log'),
-            $conf->get('resource.update_check'),
-        ) as $path) {
-            if (! is_file(realpath($path))) {
+                     $conf->getConfigFileExt(),
+                     $conf->get('resource.datastore'),
+                     $conf->get('resource.ban_file'),
+                     $conf->get('resource.log'),
+                     $conf->get('resource.update_check'),
+                 ) as $path) {
+            if (!is_file(realpath($path))) {
                 # the file may not exist yet
                 continue;
             }
 
-            if (! is_readable(realpath($path))) {
-                $errors[] = '"'.$path.'" '. t('file is not readable');
+            if (!is_readable(realpath($path))) {
+                $errors[] = '"' . $path . '" ' . t('file is not readable');
             }
-            if (! is_writable(realpath($path))) {
-                $errors[] = '"'.$path.'" '. t('file is not writable');
+            if (!is_writable(realpath($path))) {
+                $errors[] = '"' . $path . '" ' . t('file is not writable');
             }
         }
 
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 9a0fe61a..1c5b9251 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -2,7 +2,7 @@
 
 namespace Shaarli\Render;
 
-use ApplicationUtils;
+use Shaarli\ApplicationUtils;
 use Exception;
 use Shaarli\Bookmark\LinkDB;
 use RainTPL;
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
index 55251a30..89f0ff7f 100644
--- a/application/updater/Updater.php
+++ b/application/updater/Updater.php
@@ -2,7 +2,7 @@
 
 namespace Shaarli\Updater;
 
-use ApplicationUtils;
+use Shaarli\ApplicationUtils;
 use Exception;
 use RainTPL;
 use ReflectionClass;
-- 
cgit v1.2.3


From a932f486f20f3daf8ad657d8d39a6d6c316e66eb Mon Sep 17 00:00:00 2001
From: VirtualTam 
Date: Tue, 4 Dec 2018 00:02:17 +0100
Subject: namespacing: \Shaarli\Router

Signed-off-by: VirtualTam 
---
 application/Router.php | 43 ++++++++++++++++++++++---------------------
 1 file changed, 22 insertions(+), 21 deletions(-)

(limited to 'application')

diff --git a/application/Router.php b/application/Router.php
index beb3165b..05877acd 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -1,4 +1,5 @@
 
Date: Tue, 4 Dec 2018 00:13:42 +0100
Subject: namespacing: \Shaarli\Netscape\NetscapeBookmarkUtils

Signed-off-by: VirtualTam 
---
 application/NetscapeBookmarkUtils.php          | 220 ------------------------
 application/netscape/NetscapeBookmarkUtils.php | 225 +++++++++++++++++++++++++
 2 files changed, 225 insertions(+), 220 deletions(-)
 delete mode 100644 application/NetscapeBookmarkUtils.php
 create mode 100644 application/netscape/NetscapeBookmarkUtils.php

(limited to 'application')

diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
deleted file mode 100644
index e0022fe1..00000000
--- a/application/NetscapeBookmarkUtils.php
+++ /dev/null
@@ -1,220 +0,0 @@
-getTimestamp();
-            $link['taglist'] = str_replace(' ', ',', $link['tags']);
-
-            if (startsWith($link['url'], '?') && $prependNoteUrl) {
-                $link['url'] = $indexUrl . $link['url'];
-            }
-
-            $bookmarkLinks[] = $link;
-        }
-
-        return $bookmarkLinks;
-    }
-
-    /**
-     * Generates an import status summary
-     *
-     * @param string $filename       name of the file to import
-     * @param int    $filesize       size of the file to import
-     * @param int    $importCount    how many links were imported
-     * @param int    $overwriteCount how many links were overwritten
-     * @param int    $skipCount      how many links were skipped
-     * @param int    $duration       how many seconds did the import take
-     *
-     * @return string Summary of the bookmark import status
-     */
-    private static function importStatus(
-        $filename,
-        $filesize,
-        $importCount = 0,
-        $overwriteCount = 0,
-        $skipCount = 0,
-        $duration = 0
-    ) {
-        $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
-        if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
-            $status .= t('has an unknown file format. Nothing was imported.');
-        } else {
-            $status .= vsprintf(
-                t(
-                    'was successfully processed in %d seconds: '
-                    .'%d links imported, %d links overwritten, %d links skipped.'
-                ),
-                [$duration, $importCount, $overwriteCount, $skipCount]
-            );
-        }
-        return $status;
-    }
-
-    /**
-     * Imports Web bookmarks from an uploaded Netscape bookmark dump
-     *
-     * @param array         $post      Server $_POST parameters
-     * @param array         $files     Server $_FILES parameters
-     * @param LinkDB        $linkDb    Loaded LinkDB instance
-     * @param ConfigManager $conf      instance
-     * @param History       $history   History instance
-     *
-     * @return string Summary of the bookmark import status
-     */
-    public static function import($post, $files, $linkDb, $conf, $history)
-    {
-        $start = time();
-        $filename = $files['filetoupload']['name'];
-        $filesize = $files['filetoupload']['size'];
-        $data = file_get_contents($files['filetoupload']['tmp_name']);
-
-        if (preg_match('//i', $data) === 0) {
-            return self::importStatus($filename, $filesize);
-        }
-
-        // Overwrite existing links?
-        $overwrite = ! empty($post['overwrite']);
-
-        // Add tags to all imported links?
-        if (empty($post['default_tags'])) {
-            $defaultTags = array();
-        } else {
-            $defaultTags = preg_split(
-                '/[\s,]+/',
-                escape($post['default_tags'])
-            );
-        }
-
-        // links are imported as public by default
-        $defaultPrivacy = 0;
-
-        $parser = new NetscapeBookmarkParser(
-            true,                           // nested tag support
-            $defaultTags,                   // additional user-specified tags
-            strval(1 - $defaultPrivacy),    // defaultPub = 1 - defaultPrivacy
-            $conf->get('resource.data_dir') // log path, will be overridden
-        );
-        $logger = new Logger(
-            $conf->get('resource.data_dir'),
-            ! $conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
-            [
-                'prefix' => 'import.',
-                'extension' => 'log',
-            ]
-        );
-        $parser->setLogger($logger);
-        $bookmarks = $parser->parseString($data);
-
-        $importCount = 0;
-        $overwriteCount = 0;
-        $skipCount = 0;
-
-        foreach ($bookmarks as $bkm) {
-            $private = $defaultPrivacy;
-            if (empty($post['privacy']) || $post['privacy'] == 'default') {
-                // use value from the imported file
-                $private = $bkm['pub'] == '1' ? 0 : 1;
-            } elseif ($post['privacy'] == 'private') {
-                // all imported links are private
-                $private = 1;
-            } elseif ($post['privacy'] == 'public') {
-                // all imported links are public
-                $private = 0;
-            }
-
-            $newLink = array(
-                'title' => $bkm['title'],
-                'url' => $bkm['uri'],
-                'description' => $bkm['note'],
-                'private' => $private,
-                'tags' => $bkm['tags']
-            );
-
-            $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
-
-            if ($existingLink !== false) {
-                if ($overwrite === false) {
-                    // Do not overwrite an existing link
-                    $skipCount++;
-                    continue;
-                }
-
-                // Overwrite an existing link, keep its date
-                $newLink['id'] = $existingLink['id'];
-                $newLink['created'] = $existingLink['created'];
-                $newLink['updated'] = new DateTime();
-                $newLink['shorturl'] = $existingLink['shorturl'];
-                $linkDb[$existingLink['id']] = $newLink;
-                $importCount++;
-                $overwriteCount++;
-                continue;
-            }
-
-            // Add a new link - @ used for UNIX timestamps
-            $newLinkDate = new DateTime('@'.strval($bkm['time']));
-            $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
-            $newLink['created'] = $newLinkDate;
-            $newLink['id'] = $linkDb->getNextId();
-            $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
-            $linkDb[$newLink['id']] = $newLink;
-            $importCount++;
-        }
-
-        $linkDb->save($conf->get('resource.page_cache'));
-        $history->importLinks();
-
-        $duration = time() - $start;
-        return self::importStatus(
-            $filename,
-            $filesize,
-            $importCount,
-            $overwriteCount,
-            $skipCount,
-            $duration
-        );
-    }
-}
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php
new file mode 100644
index 00000000..2bf928c2
--- /dev/null
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -0,0 +1,225 @@
+getTimestamp();
+            $link['taglist'] = str_replace(' ', ',', $link['tags']);
+
+            if (startsWith($link['url'], '?') && $prependNoteUrl) {
+                $link['url'] = $indexUrl . $link['url'];
+            }
+
+            $bookmarkLinks[] = $link;
+        }
+
+        return $bookmarkLinks;
+    }
+
+    /**
+     * Generates an import status summary
+     *
+     * @param string $filename       name of the file to import
+     * @param int    $filesize       size of the file to import
+     * @param int    $importCount    how many links were imported
+     * @param int    $overwriteCount how many links were overwritten
+     * @param int    $skipCount      how many links were skipped
+     * @param int    $duration       how many seconds did the import take
+     *
+     * @return string Summary of the bookmark import status
+     */
+    private static function importStatus(
+        $filename,
+        $filesize,
+        $importCount = 0,
+        $overwriteCount = 0,
+        $skipCount = 0,
+        $duration = 0
+    ) {
+        $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
+        if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
+            $status .= t('has an unknown file format. Nothing was imported.');
+        } else {
+            $status .= vsprintf(
+                t(
+                    'was successfully processed in %d seconds: '
+                    . '%d links imported, %d links overwritten, %d links skipped.'
+                ),
+                [$duration, $importCount, $overwriteCount, $skipCount]
+            );
+        }
+        return $status;
+    }
+
+    /**
+     * Imports Web bookmarks from an uploaded Netscape bookmark dump
+     *
+     * @param array         $post    Server $_POST parameters
+     * @param array         $files   Server $_FILES parameters
+     * @param LinkDB        $linkDb  Loaded LinkDB instance
+     * @param ConfigManager $conf    instance
+     * @param History       $history History instance
+     *
+     * @return string Summary of the bookmark import status
+     */
+    public static function import($post, $files, $linkDb, $conf, $history)
+    {
+        $start = time();
+        $filename = $files['filetoupload']['name'];
+        $filesize = $files['filetoupload']['size'];
+        $data = file_get_contents($files['filetoupload']['tmp_name']);
+
+        if (preg_match('//i', $data) === 0) {
+            return self::importStatus($filename, $filesize);
+        }
+
+        // Overwrite existing links?
+        $overwrite = !empty($post['overwrite']);
+
+        // Add tags to all imported links?
+        if (empty($post['default_tags'])) {
+            $defaultTags = array();
+        } else {
+            $defaultTags = preg_split(
+                '/[\s,]+/',
+                escape($post['default_tags'])
+            );
+        }
+
+        // links are imported as public by default
+        $defaultPrivacy = 0;
+
+        $parser = new NetscapeBookmarkParser(
+            true,                           // nested tag support
+            $defaultTags,                   // additional user-specified tags
+            strval(1 - $defaultPrivacy),    // defaultPub = 1 - defaultPrivacy
+            $conf->get('resource.data_dir') // log path, will be overridden
+        );
+        $logger = new Logger(
+            $conf->get('resource.data_dir'),
+            !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
+            [
+                'prefix' => 'import.',
+                'extension' => 'log',
+            ]
+        );
+        $parser->setLogger($logger);
+        $bookmarks = $parser->parseString($data);
+
+        $importCount = 0;
+        $overwriteCount = 0;
+        $skipCount = 0;
+
+        foreach ($bookmarks as $bkm) {
+            $private = $defaultPrivacy;
+            if (empty($post['privacy']) || $post['privacy'] == 'default') {
+                // use value from the imported file
+                $private = $bkm['pub'] == '1' ? 0 : 1;
+            } elseif ($post['privacy'] == 'private') {
+                // all imported links are private
+                $private = 1;
+            } elseif ($post['privacy'] == 'public') {
+                // all imported links are public
+                $private = 0;
+            }
+
+            $newLink = array(
+                'title' => $bkm['title'],
+                'url' => $bkm['uri'],
+                'description' => $bkm['note'],
+                'private' => $private,
+                'tags' => $bkm['tags']
+            );
+
+            $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
+
+            if ($existingLink !== false) {
+                if ($overwrite === false) {
+                    // Do not overwrite an existing link
+                    $skipCount++;
+                    continue;
+                }
+
+                // Overwrite an existing link, keep its date
+                $newLink['id'] = $existingLink['id'];
+                $newLink['created'] = $existingLink['created'];
+                $newLink['updated'] = new DateTime();
+                $newLink['shorturl'] = $existingLink['shorturl'];
+                $linkDb[$existingLink['id']] = $newLink;
+                $importCount++;
+                $overwriteCount++;
+                continue;
+            }
+
+            // Add a new link - @ used for UNIX timestamps
+            $newLinkDate = new DateTime('@' . strval($bkm['time']));
+            $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
+            $newLink['created'] = $newLinkDate;
+            $newLink['id'] = $linkDb->getNextId();
+            $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
+            $linkDb[$newLink['id']] = $newLink;
+            $importCount++;
+        }
+
+        $linkDb->save($conf->get('resource.page_cache'));
+        $history->importLinks();
+
+        $duration = time() - $start;
+        return self::importStatus(
+            $filename,
+            $filesize,
+            $importCount,
+            $overwriteCount,
+            $skipCount,
+            $duration
+        );
+    }
+}
-- 
cgit v1.2.3


From e1850388348d4bfdf463a5aa341bc470da79cf32 Mon Sep 17 00:00:00 2001
From: VirtualTam 
Date: Tue, 4 Dec 2018 00:26:50 +0100
Subject: namespacing: \Shaarli\Plugin\PluginManager

Signed-off-by: VirtualTam 
---
 application/PluginManager.php                      | 244 ---------------------
 application/plugin/PluginManager.php               | 233 ++++++++++++++++++++
 .../exception/PluginFileNotFoundException.php      |  23 ++
 3 files changed, 256 insertions(+), 244 deletions(-)
 delete mode 100644 application/PluginManager.php
 create mode 100644 application/plugin/PluginManager.php
 create mode 100644 application/plugin/exception/PluginFileNotFoundException.php

(limited to 'application')

diff --git a/application/PluginManager.php b/application/PluginManager.php
deleted file mode 100644
index 1ed4db4b..00000000
--- a/application/PluginManager.php
+++ /dev/null
@@ -1,244 +0,0 @@
-conf = $conf;
-        $this->errors = array();
-    }
-
-    /**
-     * Load plugins listed in $authorizedPlugins.
-     *
-     * @param array $authorizedPlugins Names of plugin authorized to be loaded.
-     *
-     * @return void
-     */
-    public function load($authorizedPlugins)
-    {
-        $this->authorizedPlugins = $authorizedPlugins;
-
-        $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR);
-        $dirnames = array_map('basename', $dirs);
-        foreach ($this->authorizedPlugins as $plugin) {
-            $index = array_search($plugin, $dirnames);
-
-            // plugin authorized, but its folder isn't listed
-            if ($index === false) {
-                continue;
-            }
-
-            try {
-                $this->loadPlugin($dirs[$index], $plugin);
-            } catch (PluginFileNotFoundException $e) {
-                error_log($e->getMessage());
-            }
-        }
-    }
-
-    /**
-     * Execute all plugins registered hook.
-     *
-     * @param string        $hook   name of the hook to trigger.
-     * @param array         $data   list of data to manipulate passed by reference.
-     * @param array         $params additional parameters such as page target.
-     *
-     * @return void
-     */
-    public function executeHooks($hook, &$data, $params = array())
-    {
-        if (!empty($params['target'])) {
-            $data['_PAGE_'] = $params['target'];
-        }
-
-        if (isset($params['loggedin'])) {
-            $data['_LOGGEDIN_'] = $params['loggedin'];
-        }
-
-        foreach ($this->loadedPlugins as $plugin) {
-            $hookFunction = $this->buildHookName($hook, $plugin);
-
-            if (function_exists($hookFunction)) {
-                $data = call_user_func($hookFunction, $data, $this->conf);
-            }
-        }
-    }
-
-    /**
-     * Load a single plugin from its files.
-     * Call the init function if it exists, and collect errors.
-     * Add them in $loadedPlugins if successful.
-     *
-     * @param string $dir        plugin's directory.
-     * @param string $pluginName plugin's name.
-     *
-     * @return void
-     * @throws PluginFileNotFoundException - plugin files not found.
-     */
-    private function loadPlugin($dir, $pluginName)
-    {
-        if (!is_dir($dir)) {
-            throw new PluginFileNotFoundException($pluginName);
-        }
-
-        $pluginFilePath = $dir . '/' . $pluginName . '.php';
-        if (!is_file($pluginFilePath)) {
-            throw new PluginFileNotFoundException($pluginName);
-        }
-
-        $conf = $this->conf;
-        include_once $pluginFilePath;
-
-        $initFunction = $pluginName . '_init';
-        if (function_exists($initFunction)) {
-            $errors = call_user_func($initFunction, $this->conf);
-            if (!empty($errors)) {
-                $this->errors = array_merge($this->errors, $errors);
-            }
-        }
-
-        $this->loadedPlugins[] = $pluginName;
-    }
-
-    /**
-     * Construct normalize hook name for a specific plugin.
-     *
-     * Format:
-     *      hook__
-     *
-     * @param string $hook       hook name.
-     * @param string $pluginName plugin name.
-     *
-     * @return string - plugin's hook name.
-     */
-    public function buildHookName($hook, $pluginName)
-    {
-        return 'hook_' . $pluginName . '_' . $hook;
-    }
-
-    /**
-     * Retrieve plugins metadata from *.meta (INI) files into an array.
-     * Metadata contains:
-     *   - plugin description [description]
-     *   - parameters split with ';' [parameters]
-     *
-     * Respects plugins order from settings.
-     *
-     * @return array plugins metadata.
-     */
-    public function getPluginsMeta()
-    {
-        $metaData = array();
-        $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
-
-        // Browse all plugin directories.
-        foreach ($dirs as $pluginDir) {
-            $plugin = basename($pluginDir);
-            $metaFile = $pluginDir . $plugin . '.' . self::$META_EXT;
-            if (!is_file($metaFile) || !is_readable($metaFile)) {
-                continue;
-            }
-
-            $metaData[$plugin] = parse_ini_file($metaFile);
-            $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
-
-            if (isset($metaData[$plugin]['description'])) {
-                $metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
-            }
-            // Read parameters and format them into an array.
-            if (isset($metaData[$plugin]['parameters'])) {
-                $params = explode(';', $metaData[$plugin]['parameters']);
-            } else {
-                $params = array();
-            }
-            $metaData[$plugin]['parameters'] = array();
-            foreach ($params as $param) {
-                if (empty($param)) {
-                    continue;
-                }
-
-                $metaData[$plugin]['parameters'][$param]['value'] = '';
-                // Optional parameter description in parameter.PARAM_NAME=
-                if (isset($metaData[$plugin]['parameter.'. $param])) {
-                    $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
-                }
-            }
-        }
-
-        return $metaData;
-    }
-
-    /**
-     * Return the list of encountered errors.
-     *
-     * @return array List of errors (empty array if none exists).
-     */
-    public function getErrors()
-    {
-        return $this->errors;
-    }
-}
-
-/**
- * Class PluginFileNotFoundException
- *
- * Raise when plugin files can't be found.
- */
-class PluginFileNotFoundException extends Exception
-{
-    /**
-     * Construct exception with plugin name.
-     * Generate message.
-     *
-     * @param string $pluginName name of the plugin not found
-     */
-    public function __construct($pluginName)
-    {
-        $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
-    }
-}
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
new file mode 100644
index 00000000..f7b24a8e
--- /dev/null
+++ b/application/plugin/PluginManager.php
@@ -0,0 +1,233 @@
+conf = $conf;
+        $this->errors = array();
+    }
+
+    /**
+     * Load plugins listed in $authorizedPlugins.
+     *
+     * @param array $authorizedPlugins Names of plugin authorized to be loaded.
+     *
+     * @return void
+     */
+    public function load($authorizedPlugins)
+    {
+        $this->authorizedPlugins = $authorizedPlugins;
+
+        $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR);
+        $dirnames = array_map('basename', $dirs);
+        foreach ($this->authorizedPlugins as $plugin) {
+            $index = array_search($plugin, $dirnames);
+
+            // plugin authorized, but its folder isn't listed
+            if ($index === false) {
+                continue;
+            }
+
+            try {
+                $this->loadPlugin($dirs[$index], $plugin);
+            } catch (PluginFileNotFoundException $e) {
+                error_log($e->getMessage());
+            }
+        }
+    }
+
+    /**
+     * Execute all plugins registered hook.
+     *
+     * @param string $hook   name of the hook to trigger.
+     * @param array  $data   list of data to manipulate passed by reference.
+     * @param array  $params additional parameters such as page target.
+     *
+     * @return void
+     */
+    public function executeHooks($hook, &$data, $params = array())
+    {
+        if (!empty($params['target'])) {
+            $data['_PAGE_'] = $params['target'];
+        }
+
+        if (isset($params['loggedin'])) {
+            $data['_LOGGEDIN_'] = $params['loggedin'];
+        }
+
+        foreach ($this->loadedPlugins as $plugin) {
+            $hookFunction = $this->buildHookName($hook, $plugin);
+
+            if (function_exists($hookFunction)) {
+                $data = call_user_func($hookFunction, $data, $this->conf);
+            }
+        }
+    }
+
+    /**
+     * Load a single plugin from its files.
+     * Call the init function if it exists, and collect errors.
+     * Add them in $loadedPlugins if successful.
+     *
+     * @param string $dir        plugin's directory.
+     * @param string $pluginName plugin's name.
+     *
+     * @return void
+     * @throws \Shaarli\Plugin\Exception\PluginFileNotFoundException - plugin files not found.
+     */
+    private function loadPlugin($dir, $pluginName)
+    {
+        if (!is_dir($dir)) {
+            throw new PluginFileNotFoundException($pluginName);
+        }
+
+        $pluginFilePath = $dir . '/' . $pluginName . '.php';
+        if (!is_file($pluginFilePath)) {
+            throw new PluginFileNotFoundException($pluginName);
+        }
+
+        $conf = $this->conf;
+        include_once $pluginFilePath;
+
+        $initFunction = $pluginName . '_init';
+        if (function_exists($initFunction)) {
+            $errors = call_user_func($initFunction, $this->conf);
+            if (!empty($errors)) {
+                $this->errors = array_merge($this->errors, $errors);
+            }
+        }
+
+        $this->loadedPlugins[] = $pluginName;
+    }
+
+    /**
+     * Construct normalize hook name for a specific plugin.
+     *
+     * Format:
+     *      hook__
+     *
+     * @param string $hook       hook name.
+     * @param string $pluginName plugin name.
+     *
+     * @return string - plugin's hook name.
+     */
+    public function buildHookName($hook, $pluginName)
+    {
+        return 'hook_' . $pluginName . '_' . $hook;
+    }
+
+    /**
+     * Retrieve plugins metadata from *.meta (INI) files into an array.
+     * Metadata contains:
+     *   - plugin description [description]
+     *   - parameters split with ';' [parameters]
+     *
+     * Respects plugins order from settings.
+     *
+     * @return array plugins metadata.
+     */
+    public function getPluginsMeta()
+    {
+        $metaData = array();
+        $dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
+
+        // Browse all plugin directories.
+        foreach ($dirs as $pluginDir) {
+            $plugin = basename($pluginDir);
+            $metaFile = $pluginDir . $plugin . '.' . self::$META_EXT;
+            if (!is_file($metaFile) || !is_readable($metaFile)) {
+                continue;
+            }
+
+            $metaData[$plugin] = parse_ini_file($metaFile);
+            $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
+
+            if (isset($metaData[$plugin]['description'])) {
+                $metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
+            }
+            // Read parameters and format them into an array.
+            if (isset($metaData[$plugin]['parameters'])) {
+                $params = explode(';', $metaData[$plugin]['parameters']);
+            } else {
+                $params = array();
+            }
+            $metaData[$plugin]['parameters'] = array();
+            foreach ($params as $param) {
+                if (empty($param)) {
+                    continue;
+                }
+
+                $metaData[$plugin]['parameters'][$param]['value'] = '';
+                // Optional parameter description in parameter.PARAM_NAME=
+                if (isset($metaData[$plugin]['parameter.' . $param])) {
+                    $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.' . $param]);
+                }
+            }
+        }
+
+        return $metaData;
+    }
+
+    /**
+     * Return the list of encountered errors.
+     *
+     * @return array List of errors (empty array if none exists).
+     */
+    public function getErrors()
+    {
+        return $this->errors;
+    }
+}
diff --git a/application/plugin/exception/PluginFileNotFoundException.php b/application/plugin/exception/PluginFileNotFoundException.php
new file mode 100644
index 00000000..e5386f02
--- /dev/null
+++ b/application/plugin/exception/PluginFileNotFoundException.php
@@ -0,0 +1,23 @@
+message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
+    }
+}
-- 
cgit v1.2.3


From dea72c711ff740b3b829d238fcf85648465143a0 Mon Sep 17 00:00:00 2001
From: VirtualTam 
Date: Sat, 12 Jan 2019 23:55:38 +0100
Subject: Optimize and cleanup imports

Signed-off-by: VirtualTam 
---
 application/Languages.php                          |  1 -
 application/Thumbnailer.php                        |  2 +-
 application/api/ApiMiddleware.php                  |  3 +-
 application/api/ApiUtils.php                       |  2 +-
 application/api/controllers/ApiController.php      |  7 ++-
 application/api/controllers/History.php            | 69 ----------------------
 application/api/controllers/HistoryController.php  | 69 ++++++++++++++++++++++
 application/api/controllers/Tags.php               |  1 -
 .../api/exceptions/ApiLinkNotFoundException.php    |  2 -
 .../api/exceptions/ApiTagNotFoundException.php     |  2 -
 application/bookmark/LinkDB.php                    |  1 -
 application/feed/FeedBuilder.php                   |  1 -
 application/netscape/NetscapeBookmarkUtils.php     |  2 +-
 application/render/PageBuilder.php                 |  4 +-
 application/updater/Updater.php                    |  2 +-
 15 files changed, 80 insertions(+), 88 deletions(-)
 delete mode 100644 application/api/controllers/History.php
 create mode 100644 application/api/controllers/HistoryController.php

(limited to 'application')

diff --git a/application/Languages.php b/application/Languages.php
index b9c5d0e8..5cda802e 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -3,7 +3,6 @@
 namespace Shaarli;
 
 use Gettext\GettextTranslator;
-use Gettext\Merge;
 use Gettext\Translations;
 use Gettext\Translator;
 use Gettext\TranslatorInterface;
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
index 37ed97a1..a23f98e9 100644
--- a/application/Thumbnailer.php
+++ b/application/Thumbnailer.php
@@ -3,9 +3,9 @@
 namespace Shaarli;
 
 use Shaarli\Config\ConfigManager;
+use WebThumbnailer\Application\ConfigManager as WTConfigManager;
 use WebThumbnailer\Exception\WebThumbnailerException;
 use WebThumbnailer\WebThumbnailer;
-use WebThumbnailer\Application\ConfigManager as WTConfigManager;
 
 /**
  * Class Thumbnailer
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index a2101f29..5ffb8c6d 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -1,9 +1,8 @@
 history->getHistory();
-
-        // Return history operations from the {offset}th, starting from {since}.
-        $since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since'));
-        $offset = $request->getParam('offset');
-        if (empty($offset)) {
-            $offset = 0;
-        } elseif (ctype_digit($offset)) {
-            $offset = (int) $offset;
-        } else {
-            throw new ApiBadParametersException('Invalid offset');
-        }
-
-        // limit parameter is either a number of links or 'all' for everything.
-        $limit = $request->getParam('limit');
-        if (empty($limit)) {
-            $limit = count($history);
-        } elseif (ctype_digit($limit)) {
-            $limit = (int) $limit;
-        } else {
-            throw new ApiBadParametersException('Invalid limit');
-        }
-
-        $out = [];
-        $i = 0;
-        foreach ($history as $entry) {
-            if ((! empty($since) && $entry['datetime'] <= $since) || count($out) >= $limit) {
-                break;
-            }
-            if (++$i > $offset) {
-                $out[$i] = $entry;
-                $out[$i]['datetime'] = $out[$i]['datetime']->format(\DateTime::ATOM);
-            }
-        }
-        $out = array_values($out);
-
-        return $response->withJson($out, 200, $this->jsonStyle);
-    }
-}
diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php
new file mode 100644
index 00000000..9afcfa26
--- /dev/null
+++ b/application/api/controllers/HistoryController.php
@@ -0,0 +1,69 @@
+history->getHistory();
+
+        // Return history operations from the {offset}th, starting from {since}.
+        $since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since'));
+        $offset = $request->getParam('offset');
+        if (empty($offset)) {
+            $offset = 0;
+        } elseif (ctype_digit($offset)) {
+            $offset = (int) $offset;
+        } else {
+            throw new ApiBadParametersException('Invalid offset');
+        }
+
+        // limit parameter is either a number of links or 'all' for everything.
+        $limit = $request->getParam('limit');
+        if (empty($limit)) {
+            $limit = count($history);
+        } elseif (ctype_digit($limit)) {
+            $limit = (int) $limit;
+        } else {
+            throw new ApiBadParametersException('Invalid limit');
+        }
+
+        $out = [];
+        $i = 0;
+        foreach ($history as $entry) {
+            if ((! empty($since) && $entry['datetime'] <= $since) || count($out) >= $limit) {
+                break;
+            }
+            if (++$i > $offset) {
+                $out[$i] = $entry;
+                $out[$i]['datetime'] = $out[$i]['datetime']->format(\DateTime::ATOM);
+            }
+        }
+        $out = array_values($out);
+
+        return $response->withJson($out, 200, $this->jsonStyle);
+    }
+}
diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php
index 6dd78750..82f3ef74 100644
--- a/application/api/controllers/Tags.php
+++ b/application/api/controllers/Tags.php
@@ -4,7 +4,6 @@ namespace Shaarli\Api\Controllers;
 
 use Shaarli\Api\ApiUtils;
 use Shaarli\Api\Exceptions\ApiBadParametersException;
-use Shaarli\Api\Exceptions\ApiLinkNotFoundException;
 use Shaarli\Api\Exceptions\ApiTagNotFoundException;
 use Slim\Http\Request;
 use Slim\Http\Response;
diff --git a/application/api/exceptions/ApiLinkNotFoundException.php b/application/api/exceptions/ApiLinkNotFoundException.php
index c727f4f0..7c2bb56e 100644
--- a/application/api/exceptions/ApiLinkNotFoundException.php
+++ b/application/api/exceptions/ApiLinkNotFoundException.php
@@ -2,8 +2,6 @@
 
 namespace Shaarli\Api\Exceptions;
 
-use Slim\Http\Response;
-
 /**
  * Class ApiLinkNotFoundException
  *
diff --git a/application/api/exceptions/ApiTagNotFoundException.php b/application/api/exceptions/ApiTagNotFoundException.php
index eee152fe..66ace8bf 100644
--- a/application/api/exceptions/ApiTagNotFoundException.php
+++ b/application/api/exceptions/ApiTagNotFoundException.php
@@ -2,8 +2,6 @@
 
 namespace Shaarli\Api\Exceptions;
 
-use Slim\Http\Response;
-
 /**
  * Class ApiTagNotFoundException
  *
diff --git a/application/bookmark/LinkDB.php b/application/bookmark/LinkDB.php
index 6041c088..c13a1141 100644
--- a/application/bookmark/LinkDB.php
+++ b/application/bookmark/LinkDB.php
@@ -6,7 +6,6 @@ use ArrayAccess;
 use Countable;
 use DateTime;
 use Iterator;
-use Shaarli\Bookmark\LinkFilter;
 use Shaarli\Bookmark\Exception\LinkNotFoundException;
 use Shaarli\Exceptions\IOException;
 use Shaarli\FileUtils;
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
index 737a3128..b66f2f91 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -2,7 +2,6 @@
 namespace Shaarli\Feed;
 
 use DateTime;
-use Shaarli\Bookmark\LinkDB;
 
 /**
  * FeedBuilder class.
diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php
index 2bf928c2..2fb1a4a6 100644
--- a/application/netscape/NetscapeBookmarkUtils.php
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -5,12 +5,12 @@ namespace Shaarli\Netscape;
 use DateTime;
 use DateTimeZone;
 use Exception;
+use Katzgrau\KLogger\Logger;
 use Psr\Log\LogLevel;
 use Shaarli\Bookmark\LinkDB;
 use Shaarli\Config\ConfigManager;
 use Shaarli\History;
 use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser;
-use Katzgrau\KLogger\Logger;
 
 /**
  * Utilities to import and export bookmarks using the Netscape format
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 1c5b9251..0569b67f 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -2,10 +2,10 @@
 
 namespace Shaarli\Render;
 
-use Shaarli\ApplicationUtils;
 use Exception;
-use Shaarli\Bookmark\LinkDB;
 use RainTPL;
+use Shaarli\ApplicationUtils;
+use Shaarli\Bookmark\LinkDB;
 use Shaarli\Config\ConfigManager;
 use Shaarli\Thumbnailer;
 
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
index 89f0ff7f..f12e3516 100644
--- a/application/updater/Updater.php
+++ b/application/updater/Updater.php
@@ -2,12 +2,12 @@
 
 namespace Shaarli\Updater;
 
-use Shaarli\ApplicationUtils;
 use Exception;
 use RainTPL;
 use ReflectionClass;
 use ReflectionException;
 use ReflectionMethod;
+use Shaarli\ApplicationUtils;
 use Shaarli\Bookmark\LinkDB;
 use Shaarli\Bookmark\LinkFilter;
 use Shaarli\Config\ConfigJson;
-- 
cgit v1.2.3


From 5bd62b5d538a5e375bebbcdea0ffef3384143895 Mon Sep 17 00:00:00 2001
From: ArthurHoaro 
Date: Sat, 9 Feb 2019 13:05:37 +0100
Subject: Fix thumbnails disabling if PHP GD is not installed

---
 application/Thumbnailer.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'application')

diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
index a23f98e9..d5f5ac28 100644
--- a/application/Thumbnailer.php
+++ b/application/Thumbnailer.php
@@ -55,7 +55,7 @@ class Thumbnailer
         $this->conf = $conf;
 
         if (! $this->checkRequirements()) {
-            $this->conf->set('thumbnails.enabled', false);
+            $this->conf->set('thumbnails.mode', Thumbnailer::MODE_NONE);
             $this->conf->write(true);
             // TODO: create a proper error handling system able to catch exceptions...
             die(t(
-- 
cgit v1.2.3


From 520d29578c57e476ece3bdd20c286d196b7b61b4 Mon Sep 17 00:00:00 2001
From: ArthurHoaro 
Date: Sat, 9 Feb 2019 13:52:12 +0100
Subject: Remove the redirector setting

Fixes #1239
---
 application/api/ApiMiddleware.php    |  4 +---
 application/bookmark/LinkDB.php      | 41 ++++++------------------------------
 application/bookmark/LinkUtils.php   | 24 ++++-----------------
 application/config/ConfigManager.php |  4 ----
 application/feed/FeedBuilder.php     |  2 +-
 application/updater/Updater.php      | 11 +++++++++-
 6 files changed, 22 insertions(+), 64 deletions(-)

(limited to 'application')

diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 5ffb8c6d..2d55bda6 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -129,9 +129,7 @@ class ApiMiddleware
         $linkDb = new \Shaarli\Bookmark\LinkDB(
             $conf->get('resource.datastore'),
             true,
-            $conf->get('privacy.hide_public_links'),
-            $conf->get('redirector.url'),
-            $conf->get('redirector.encode_url')
+            $conf->get('privacy.hide_public_links')
         );
         $this->container['db'] = $linkDb;
     }
diff --git a/application/bookmark/LinkDB.php b/application/bookmark/LinkDB.php
index c13a1141..266632e3 100644
--- a/application/bookmark/LinkDB.php
+++ b/application/bookmark/LinkDB.php
@@ -29,10 +29,10 @@ use Shaarli\FileUtils;
  *  - 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. 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.
+ *  - url       URL of the link. Used for displayable links.
+ *              Can be absolute or relative in the database but the relative links
+ *              will be converted to absolute ones in templates.
+ *  - real_url  Raw URL in stored in the DB (absolute or relative).
  *  - shorturl  Permalink smallhash
  *
  * Implements 3 interfaces:
@@ -88,19 +88,6 @@ class LinkDB implements Iterator, Countable, ArrayAccess
     // Hide public links
     private $hidePublicLinks;
 
-    // link redirector set in user settings.
-    private $redirector;
-
-    /**
-     * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched.
-     *
-     * Example:
-     *   anonym.to needs clean URL while dereferer.org needs urlencoded URL.
-     *
-     * @var boolean $redirectorEncode parameter: true or false
-     */
-    private $redirectorEncode;
-
     /**
      * Creates a new LinkDB
      *
@@ -109,22 +96,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
      * @param string  $datastore        datastore file path.
      * @param boolean $isLoggedIn       is the user logged in?
      * @param boolean $hidePublicLinks  if true all links are private.
-     * @param string  $redirector       link redirector set in user settings.
-     * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true).
      */
     public function __construct(
         $datastore,
         $isLoggedIn,
-        $hidePublicLinks,
-        $redirector = '',
-        $redirectorEncode = true
+        $hidePublicLinks
     ) {
     
         $this->datastore = $datastore;
         $this->loggedIn = $isLoggedIn;
         $this->hidePublicLinks = $hidePublicLinks;
-        $this->redirector = $redirector;
-        $this->redirectorEncode = $redirectorEncode === true;
         $this->check();
         $this->read();
     }
@@ -323,17 +304,7 @@ You use the community supported version of the original Shaarli project, by Seba
                 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
             }
 
-            // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
-            if (!empty($this->redirector) && !startsWith($link['url'], '?')) {
-                $link['real_url'] = $this->redirector;
-                if ($this->redirectorEncode) {
-                    $link['real_url'] .= urlencode(unescape($link['url']));
-                } else {
-                    $link['real_url'] .= $link['url'];
-                }
-            } else {
-                $link['real_url'] = $link['url'];
-            }
+            $link['real_url'] = $link['url'];
 
             // To be able to load links before running the update, and prepare the update
             if (!isset($link['created'])) {
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index de5b61cb..988970bd 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -133,29 +133,15 @@ function count_private($links)
  * In a string, converts URLs to clickable links.
  *
  * @param string $text       input string.
- * @param string $redirector if a redirector is set, use it to gerenate links.
- * @param bool   $urlEncode  Use `urlencode()` on the URL after the redirector or not.
  *
  * @return string returns $text with all links converted to HTML links.
  *
  * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
  */
-function text2clickable($text, $redirector = '', $urlEncode = true)
+function text2clickable($text)
 {
     $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
-
-    if (empty($redirector)) {
-        return preg_replace($regex, '$1', $text);
-    }
-    // Redirector is set, urlencode the final URL.
-    return preg_replace_callback(
-        $regex,
-        function ($matches) use ($redirector, $urlEncode) {
-            $url = $urlEncode ? urlencode($matches[1]) : $matches[1];
-            return ''. $matches[1] .'';
-        },
-        $text
-    );
+    return preg_replace($regex, '$1', $text);
 }
 
 /**
@@ -197,15 +183,13 @@ function space2nbsp($text)
  * Format Shaarli's description
  *
  * @param string $description shaare's description.
- * @param string $redirector  if a redirector is set, use it to gerenate links.
- * @param bool   $urlEncode   Use `urlencode()` on the URL after the redirector or not.
  * @param string $indexUrl    URL to Shaarli's index.
 
  * @return string formatted description.
  */
-function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '')
+function format_description($description, $indexUrl = '')
 {
-    return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
+    return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl)));
 }
 
 /**
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index e6c35073..30993928 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -221,7 +221,6 @@ class ConfigManager
             'general.title',
             'general.header_link',
             'privacy.default_private_links',
-            'redirector.url',
         );
 
         // Only logged in user can alter config.
@@ -381,9 +380,6 @@ class ConfigManager
         // default state of the 'remember me' checkbox of the login form
         $this->setEmpty('privacy.remember_user_default', true);
 
-        $this->setEmpty('redirector.url', '');
-        $this->setEmpty('redirector.encode_url', true);
-
         $this->setEmpty('thumbnails.width', '125');
         $this->setEmpty('thumbnails.height', '90');
 
diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php
index b66f2f91..e23b3452 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -156,7 +156,7 @@ class FeedBuilder
         } else {
             $permalink = '' . t('Permalink') . '';
         }
-        $link['description'] = format_description($link['description'], '', false, $pageaddr);
+        $link['description'] = format_description($link['description'], $pageaddr);
         $link['description'] .= PHP_EOL . '
— ' . $permalink; $pubDate = $link['created']; diff --git a/application/updater/Updater.php b/application/updater/Updater.php index f12e3516..beb9ea9b 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -218,7 +218,6 @@ class Updater try { $this->conf->set('general.title', escape($this->conf->get('general.title'))); $this->conf->set('general.header_link', escape($this->conf->get('general.header_link'))); - $this->conf->set('redirector.url', escape($this->conf->get('redirector.url'))); $this->conf->write($this->isLoggedIn); } catch (Exception $e) { error_log($e->getMessage()); @@ -550,4 +549,14 @@ class Updater return true; } + + /** + * Remove redirector settings. + */ + public function updateMethodRemoveRedirector() + { + $this->conf->remove('redirector'); + $this->conf->write(true); + return true; + } } -- cgit v1.2.3 From b790f900c937d0d8f6eccc15d2b4c26023f3d276 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 9 Feb 2019 14:04:16 +0100 Subject: Fix a warning if links sticky status isn't set - initiate its status to false when the link is created - if not defined, initiate its status to false (can happen if the updater hasn't run) --- application/bookmark/LinkDB.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'application') diff --git a/application/bookmark/LinkDB.php b/application/bookmark/LinkDB.php index c13a1141..41d5591f 100644 --- a/application/bookmark/LinkDB.php +++ b/application/bookmark/LinkDB.php @@ -271,7 +271,8 @@ You use the community supported version of the original Shaarli project, by Seba ), 'private' => 0, 'created' => new DateTime(), - 'tags' => 'opensource software' + 'tags' => 'opensource software', + 'sticky' => false, ); $link['shorturl'] = link_small_hash($link['created'], $link['id']); $this->links[1] = $link; @@ -284,6 +285,7 @@ You use the community supported version of the original Shaarli project, by Seba 'private' => 1, 'created' => new DateTime('1 minute ago'), 'tags' => 'secretstuff', + 'sticky' => false, ); $link['shorturl'] = link_small_hash($link['created'], $link['id']); $this->links[0] = $link; @@ -335,6 +337,8 @@ You use the community supported version of the original Shaarli project, by Seba $link['real_url'] = $link['url']; } + $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false; + // To be able to load links before running the update, and prepare the update if (!isset($link['created'])) { $link['id'] = $link['linkdate']; -- cgit v1.2.3 From cb974e47476284d967483f7478e3dcb5a2d974d4 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 9 Feb 2019 14:29:35 +0100 Subject: Accessibility: specify the HTML lang attribute The lang is based on the user defined one. If the language is automatic, no language will be specified. Fixes #1216 --- application/render/PageBuilder.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'application') diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php index 0569b67f..3f86fc26 100644 --- a/application/render/PageBuilder.php +++ b/application/render/PageBuilder.php @@ -123,6 +123,8 @@ class PageBuilder $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); $this->tpl->assign('token', $this->token); + $this->tpl->assign('language', $this->conf->get('translation.language')); + if ($this->linkDB !== null) { $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); } -- cgit v1.2.3 From b49a04f796b9ad8533c53fee541132a4c2214909 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 9 Feb 2019 16:44:48 +0100 Subject: Rewrite IP ban management This adds a dedicated manager class to handle all ban interactions, which is instantiated and handled by LoginManager. IPs are now stored in the same format as the datastore, through FileUtils. Fixes #1032 #587 --- application/security/BanManager.php | 213 ++++++++++++++++++++++++++++++++++ application/security/LoginManager.php | 100 +++------------- 2 files changed, 227 insertions(+), 86 deletions(-) create mode 100644 application/security/BanManager.php (limited to 'application') diff --git a/application/security/BanManager.php b/application/security/BanManager.php new file mode 100644 index 00000000..68190c54 --- /dev/null +++ b/application/security/BanManager.php @@ -0,0 +1,213 @@ +trustedProxies = $trustedProxies; + $this->nbAttempts = $nbAttempts; + $this->banDuration = $banDuration; + $this->banFile = $banFile; + $this->logFile = $logFile; + $this->readBanFile(); + } + + /** + * Handle a failed login and ban the IP after too many failed attempts + * + * @param array $server The $_SERVER array + */ + public function handleFailedAttempt($server) + { + $ip = $this->getIp($server); + // the IP is behind a trusted forward proxy, but is not forwarded + // in the HTTP headers, so we do nothing + if (empty($ip)) { + return; + } + + // increment the fail count for this IP + if (isset($this->failures[$ip])) { + $this->failures[$ip]++; + } else { + $this->failures[$ip] = 1; + } + + if ($this->failures[$ip] >= $this->nbAttempts) { + $this->bans[$ip] = time() + $this->banDuration; + logm( + $this->logFile, + $server['REMOTE_ADDR'], + 'IP address banned from login: '. $ip + ); + } + $this->writeBanFile(); + } + + /** + * Remove failed attempts for the provided client. + * + * @param array $server $_SERVER + */ + public function clearFailures($server) + { + $ip = $this->getIp($server); + // the IP is behind a trusted forward proxy, but is not forwarded + // in the HTTP headers, so we do nothing + if (empty($ip)) { + return; + } + + if (isset($this->failures[$ip])) { + unset($this->failures[$ip]); + } + $this->writeBanFile(); + } + + /** + * Check whether the client IP is banned or not. + * + * @param array $server $_SERVER + * + * @return bool True if the IP is banned, false otherwise + */ + public function isBanned($server) + { + $ip = $this->getIp($server); + // the IP is behind a trusted forward proxy, but is not forwarded + // in the HTTP headers, so we allow the authentication attempt. + if (empty($ip)) { + return false; + } + + // the user is not banned + if (! isset($this->bans[$ip])) { + return false; + } + + // the user is still banned + if ($this->bans[$ip] > time()) { + return true; + } + + // the ban has expired, the user can attempt to log in again + if (isset($this->failures[$ip])) { + unset($this->failures[$ip]); + } + unset($this->bans[$ip]); + logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); + + $this->writeBanFile(); + return false; + } + + /** + * Retrieve the IP from $_SERVER. + * If the actual IP is behind an allowed reverse proxy, + * we try to extract the forwarded IP from HTTP headers. + * + * @param array $server $_SERVER + * + * @return string|bool The IP or false if none could be extracted + */ + protected function getIp($server) + { + $ip = $server['REMOTE_ADDR']; + if (! in_array($ip, $this->trustedProxies)) { + return $ip; + } + return getIpAddressFromProxy($server, $this->trustedProxies); + } + + /** + * Read a file containing banned IPs + */ + protected function readBanFile() + { + $data = FileUtils::readFlatDB($this->banFile); + if (isset($data['failures']) && is_array($data['failures'])) { + $this->failures = $data['failures']; + } + + if (isset($data['bans']) && is_array($data['bans'])) { + $this->bans = $data['bans']; + } + } + + /** + * Write the banned IPs to a file + */ + protected function writeBanFile() + { + return FileUtils::writeFlatDB( + $this->banFile, + [ + 'failures' => $this->failures, + 'bans' => $this->bans, + ] + ); + } + + /** + * Get the Failures (for UT purpose). + * + * @return array + */ + public function getFailures() + { + return $this->failures; + } + + /** + * Get the Bans (for UT purpose). + * + * @return array + */ + public function getBans() + { + return $this->bans; + } +} diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 1ff3d0be..0b0ce0b1 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -20,8 +20,8 @@ class LoginManager /** @var SessionManager Session Manager instance **/ protected $sessionManager = null; - /** @var string Path to the file containing IP bans */ - protected $banFile = ''; + /** @var BanManager Ban Manager instance **/ + protected $banManager; /** @var bool Whether the user is logged in **/ protected $isLoggedIn = false; @@ -35,17 +35,21 @@ class LoginManager /** * Constructor * - * @param array $globals The $GLOBALS array (reference) * @param ConfigManager $configManager Configuration Manager instance * @param SessionManager $sessionManager SessionManager instance */ - public function __construct(& $globals, $configManager, $sessionManager) + public function __construct($configManager, $sessionManager) { - $this->globals = &$globals; $this->configManager = $configManager; $this->sessionManager = $sessionManager; - $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); - $this->readBanFile(); + $this->banManager = new BanManager( + $this->configManager->get('security.trusted_proxies', []), + $this->configManager->get('security.ban_after'), + $this->configManager->get('security.ban_duration'), + $this->configManager->get('resource.ban_file', 'data/ipbans.php'), + $this->configManager->get('resource.log') + ); + if ($this->configManager->get('security.open_shaarli') === true) { $this->openShaarli = true; } @@ -157,31 +161,6 @@ class LoginManager return true; } - /** - * Read a file containing banned IPs - */ - protected function readBanFile() - { - if (! file_exists($this->banFile)) { - return; - } - include $this->banFile; - } - - /** - * Write the banned IPs to a file - */ - protected function writeBanFile() - { - if (! array_key_exists('IPBANS', $this->globals)) { - return; - } - file_put_contents( - $this->banFile, - "globals['IPBANS'], true) . ";\n?>" - ); - } - /** * Handle a failed login and ban the IP after too many failed attempts * @@ -189,34 +168,7 @@ class LoginManager */ public function handleFailedLogin($server) { - $ip = $server['REMOTE_ADDR']; - $trusted = $this->configManager->get('security.trusted_proxies', []); - - if (in_array($ip, $trusted)) { - $ip = getIpAddressFromProxy($server, $trusted); - if (! $ip) { - // the IP is behind a trusted forward proxy, but is not forwarded - // in the HTTP headers, so we do nothing - return; - } - } - - // increment the fail count for this IP - if (isset($this->globals['IPBANS']['FAILURES'][$ip])) { - $this->globals['IPBANS']['FAILURES'][$ip]++; - } else { - $this->globals['IPBANS']['FAILURES'][$ip] = 1; - } - - if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) { - $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800); - logm( - $this->configManager->get('resource.log'), - $server['REMOTE_ADDR'], - 'IP address banned from login' - ); - } - $this->writeBanFile(); + $this->banManager->handleFailedAttempt($server); } /** @@ -226,13 +178,7 @@ class LoginManager */ public function handleSuccessfulLogin($server) { - $ip = $server['REMOTE_ADDR']; - // FIXME unban when behind a trusted proxy? - - unset($this->globals['IPBANS']['FAILURES'][$ip]); - unset($this->globals['IPBANS']['BANS'][$ip]); - - $this->writeBanFile(); + $this->banManager->clearFailures($server); } /** @@ -244,24 +190,6 @@ class LoginManager */ public function canLogin($server) { - $ip = $server['REMOTE_ADDR']; - - if (! isset($this->globals['IPBANS']['BANS'][$ip])) { - // the user is not banned - return true; - } - - if ($this->globals['IPBANS']['BANS'][$ip] > time()) { - // the user is still banned - return false; - } - - // the ban has expired, the user can attempt to log in again - logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.'); - unset($this->globals['IPBANS']['FAILURES'][$ip]); - unset($this->globals['IPBANS']['BANS'][$ip]); - - $this->writeBanFile(); - return true; + return ! $this->banManager->isBanned($server); } } -- cgit v1.2.3 From 8d03f705ebbc891e216d509d4de0419842ebd317 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 9 Feb 2019 17:59:53 +0100 Subject: Bulk action: set visibility Added 2 buttons when link checkboxes are checked to set them either public or private. Related to #572 #1160 --- application/Router.php | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'application') diff --git a/application/Router.php b/application/Router.php index 05877acd..d7187487 100644 --- a/application/Router.php +++ b/application/Router.php @@ -38,6 +38,8 @@ class Router public static $PAGE_DELETELINK = 'delete_link'; + public static $PAGE_CHANGE_VISIBILITY = 'change_visibility'; + public static $PAGE_PINLINK = 'pin'; public static $PAGE_EXPORT = 'export'; @@ -149,6 +151,10 @@ class Router return self::$PAGE_DELETELINK; } + if (isset($get[self::$PAGE_CHANGE_VISIBILITY])) { + return self::$PAGE_CHANGE_VISIBILITY; + } + if (startsWith($query, 'do=' . self::$PAGE_PINLINK)) { return self::$PAGE_PINLINK; } -- cgit v1.2.3 From a8e7da01146455f13ef06b151a7dafedd3acf769 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 9 Feb 2019 14:13:08 +0100 Subject: Do not try to retrieve thumbnails for internal link Also adds a helper function to determine if a link is a note and apply it across multiple files. --- application/api/ApiUtils.php | 2 +- application/bookmark/LinkUtils.php | 13 +++++++++++++ application/feed/FeedBuilder.php | 4 ++-- application/netscape/NetscapeBookmarkUtils.php | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) (limited to 'application') diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index 1824b5d0..1e3ac02e 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -59,7 +59,7 @@ class ApiUtils { $out['id'] = $link['id']; // Not an internal link - if ($link['url'][0] != '?') { + if (! is_note($link['url'])) { $out['url'] = $link['url']; } else { $out['url'] = $indexUrl . $link['url']; diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index de5b61cb..9e9d4f0a 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -220,3 +220,16 @@ function link_small_hash($date, $id) { return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id); } + +/** + * Returns whether or not the link is an internal note. + * Its URL starts by `?` because it's actually a permalink. + * + * @param string $linkUrl + * + * @return bool true if internal note, false otherwise. + */ +function is_note($linkUrl) +{ + return isset($linkUrl[0]) && $linkUrl[0] === '?'; +} diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index b66f2f91..fec0452b 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php @@ -147,8 +147,8 @@ class FeedBuilder protected function buildItem($link, $pageaddr) { $link['guid'] = $pageaddr . '?' . $link['shorturl']; - // Check for both signs of a note: starting with ? and 7 chars long. - if ($link['url'][0] === '?' && strlen($link['url']) === 7) { + // Prepend the root URL for notes + if (is_note($link['url'])) { $link['url'] = $pageaddr . $link['url']; } if ($this->usePermalinks === true) { diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index 2fb1a4a6..28665941 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -54,7 +54,7 @@ class NetscapeBookmarkUtils $link['timestamp'] = $date->getTimestamp(); $link['taglist'] = str_replace(' ', ',', $link['tags']); - if (startsWith($link['url'], '?') && $prependNoteUrl) { + if (is_note($link['url']) && $prependNoteUrl) { $link['url'] = $indexUrl . $link['url']; } -- cgit v1.2.3 From 6a4872520cbbc012b5a8358cd50c78844afe8d07 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Sat, 8 Jun 2019 13:59:19 +0200 Subject: Automatically retrieve description for new bookmarks If the option is enabled, it will try to find a meta tag containing the page description and keywords, just like we do for the page title. It will either look for regular meta tag or OpenGraph ones. The option is disabled by default. Note that keywords meta tags is mostly not used. In `configure` template, the variable associated with this setting is `$retrieve_description`. Fixes #1302 --- application/bookmark/LinkUtils.php | 85 ++++++++++++++++++++++++++++++++++-- application/config/ConfigManager.php | 1 + 2 files changed, 82 insertions(+), 4 deletions(-) (limited to 'application') diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index 35a5b290..77eb2d95 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -7,13 +7,25 @@ use Shaarli\Bookmark\LinkDB; * * @param string $charset to extract from the downloaded page (reference) * @param string $title to extract from the downloaded page (reference) + * @param string $description to extract from the downloaded page (reference) + * @param string $keywords to extract from the downloaded page (reference) + * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content * @param string $curlGetInfo Optionally overrides curl_getinfo function * * @return Closure */ -function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo') -{ +function get_curl_download_callback( + &$charset, + &$title, + &$description, + &$keywords, + $retrieveDescription, + $curlGetInfo = 'curl_getinfo' +) { $isRedirected = false; + $currentChunk = 0; + $foundChunk = null; + /** * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). * @@ -25,7 +37,18 @@ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_get * * @return int|bool length of $data or false if we need to stop the download */ - return function (&$ch, $data) use ($curlGetInfo, &$charset, &$title, &$isRedirected) { + return function (&$ch, $data) use ( + $retrieveDescription, + $curlGetInfo, + &$charset, + &$title, + &$description, + &$keywords, + &$isRedirected, + &$currentChunk, + &$foundChunk + ) { + $currentChunk++; $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); if (!empty($responseCode) && in_array($responseCode, [301, 302])) { $isRedirected = true; @@ -50,9 +73,34 @@ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_get } if (empty($title)) { $title = html_extract_title($data); + $foundChunk = ! empty($title) ? $currentChunk : $foundChunk; + } + if ($retrieveDescription && empty($description)) { + $description = html_extract_tag('description', $data); + $foundChunk = ! empty($description) ? $currentChunk : $foundChunk; } + if ($retrieveDescription && empty($keywords)) { + $keywords = html_extract_tag('keywords', $data); + if (! empty($keywords)) { + $foundChunk = $currentChunk; + // Keywords use the format tag1, tag2 multiple words, tag + // So we format them to match Shaarli's separator and glue multiple words with '-' + $keywords = implode(' ', array_map(function($keyword) { + return implode('-', preg_split('/\s+/', trim($keyword))); + }, explode(',', $keywords))); + } + } + // We got everything we want, stop the download. - if (!empty($responseCode) && !empty($contentType) && !empty($charset) && !empty($title)) { + // If we already found either the title, description or keywords, + // it's highly unlikely that we'll found the other metas further than + // in the same chunk of data or the next one. So we also stop the download after that. + if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null + && (! $retrieveDescription + || $foundChunk < $currentChunk + || (!empty($title) && !empty($description) && !empty($keywords)) + ) + ) { return false; } @@ -110,6 +158,35 @@ function html_extract_charset($html) return false; } +/** + * Extract meta tag from HTML content in either: + * - OpenGraph: + * - Meta tag: + * + * @param string $tag Name of the tag to retrieve. + * @param string $html HTML content where to look for charset. + * + * @return bool|string Charset string if found, false otherwise. + */ +function html_extract_tag($tag, $html) +{ + $propertiesKey = ['property', 'name', 'itemprop']; + $properties = implode('|', $propertiesKey); + // Try to retrieve OpenGraph image. + $ogRegex = '#]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#'; + // If the attributes are not in the order property => content (e.g. Github) + // New regex to keep this readable... more or less. + $ogRegexReverse = '#]+content=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#'; + + if (preg_match($ogRegex, $html, $matches) > 0 + || preg_match($ogRegexReverse, $html, $matches) > 0 + ) { + return $matches[1]; + } + + return false; +} + /** * Count private links in given linklist. * diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index 30993928..c95e6800 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -365,6 +365,7 @@ class ConfigManager $this->setEmpty('general.links_per_page', 20); $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); $this->setEmpty('general.default_note_title', 'Note: '); + $this->setEmpty('general.retrieve_description', false); $this->setEmpty('updates.check_updates', false); $this->setEmpty('updates.check_updates_branch', 'stable'); -- cgit v1.2.3