aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/api/ApiUtils.php21
-rw-r--r--application/api/controllers/Links.php12
-rw-r--r--application/bookmark/LinkUtils.php6
-rw-r--r--application/feed/CachedPage.php45
-rw-r--r--application/front/controller/admin/ShaarePublishController.php2
-rw-r--r--application/front/controller/visitor/DailyController.php5
-rw-r--r--application/helper/DailyPageHelper.php66
-rw-r--r--application/http/MetadataRetriever.php9
-rw-r--r--application/render/PageCacheManager.php14
9 files changed, 131 insertions, 49 deletions
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index 05a2840a..9228bb2d 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -91,13 +91,17 @@ class ApiUtils
91 * If no URL is provided, it will generate a local note URL. 91 * If no URL is provided, it will generate a local note URL.
92 * If no title is provided, it will use the URL as title. 92 * If no title is provided, it will use the URL as title.
93 * 93 *
94 * @param array|null $input Request Link. 94 * @param array|null $input Request Link.
95 * @param bool $defaultPrivate Setting defined if a bookmark is private by default. 95 * @param bool $defaultPrivate Setting defined if a bookmark is private by default.
96 * @param string $tagsSeparator Tags separator loaded from the config file.
96 * 97 *
97 * @return Bookmark instance. 98 * @return Bookmark instance.
98 */ 99 */
99 public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark 100 public static function buildBookmarkFromRequest(
100 { 101 ?array $input,
102 bool $defaultPrivate,
103 string $tagsSeparator
104 ): Bookmark {
101 $bookmark = new Bookmark(); 105 $bookmark = new Bookmark();
102 $url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; 106 $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
103 if (isset($input['private'])) { 107 if (isset($input['private'])) {
@@ -109,6 +113,15 @@ class ApiUtils
109 $bookmark->setTitle(! empty($input['title']) ? $input['title'] : ''); 113 $bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
110 $bookmark->setUrl($url); 114 $bookmark->setUrl($url);
111 $bookmark->setDescription(! empty($input['description']) ? $input['description'] : ''); 115 $bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
116
117 // Be permissive with provided tags format
118 if (is_string($input['tags'] ?? null)) {
119 $input['tags'] = tags_str2array($input['tags'], $tagsSeparator);
120 }
121 if (is_array($input['tags'] ?? null) && count($input['tags']) === 1 && is_string($input['tags'][0])) {
122 $input['tags'] = tags_str2array($input['tags'][0], $tagsSeparator);
123 }
124
112 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); 125 $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
113 $bookmark->setPrivate($private); 126 $bookmark->setPrivate($private);
114 127
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index c379b962..b83b2260 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -117,7 +117,11 @@ class Links extends ApiController
117 public function postLink($request, $response) 117 public function postLink($request, $response)
118 { 118 {
119 $data = (array) ($request->getParsedBody() ?? []); 119 $data = (array) ($request->getParsedBody() ?? []);
120 $bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); 120 $bookmark = ApiUtils::buildBookmarkFromRequest(
121 $data,
122 $this->conf->get('privacy.default_private_links'),
123 $this->conf->get('general.tags_separator', ' ')
124 );
121 // duplicate by URL, return 409 Conflict 125 // duplicate by URL, return 409 Conflict
122 if ( 126 if (
123 ! empty($bookmark->getUrl()) 127 ! empty($bookmark->getUrl())
@@ -158,7 +162,11 @@ class Links extends ApiController
158 $index = index_url($this->ci['environment']); 162 $index = index_url($this->ci['environment']);
159 $data = $request->getParsedBody(); 163 $data = $request->getParsedBody();
160 164
161 $requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); 165 $requestBookmark = ApiUtils::buildBookmarkFromRequest(
166 $data,
167 $this->conf->get('privacy.default_private_links'),
168 $this->conf->get('general.tags_separator', ' ')
169 );
162 // duplicate URL on a different link, return 409 Conflict 170 // duplicate URL on a different link, return 409 Conflict
163 if ( 171 if (
164 ! empty($requestBookmark->getUrl()) 172 ! empty($requestBookmark->getUrl())
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index d65e97ed..0ab2d213 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -68,11 +68,13 @@ function html_extract_tag($tag, $html)
68 $properties = implode('|', $propertiesKey); 68 $properties = implode('|', $propertiesKey);
69 // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' 69 // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
70 $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; 70 $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
71 // Support quotes in double quoted content, and the other way around
72 $content = 'content=(["\'])((?:(?!\1).)*)\1';
71 // Try to retrieve OpenGraph tag. 73 // Try to retrieve OpenGraph tag.
72 $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; 74 $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#';
73 // If the attributes are not in the order property => content (e.g. Github) 75 // If the attributes are not in the order property => content (e.g. Github)
74 // New regex to keep this readable... more or less. 76 // New regex to keep this readable... more or less.
75 $ogRegexReverse = '#<meta[^>]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; 77 $ogRegexReverse = '#<meta[^>]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
76 78
77 if ( 79 if (
78 preg_match($ogRegex, $html, $matches) > 0 80 preg_match($ogRegex, $html, $matches) > 0
diff --git a/application/feed/CachedPage.php b/application/feed/CachedPage.php
index d809bdd9..c23c200f 100644
--- a/application/feed/CachedPage.php
+++ b/application/feed/CachedPage.php
@@ -1,34 +1,43 @@
1<?php 1<?php
2 2
3declare(strict_types=1);
4
3namespace Shaarli\Feed; 5namespace Shaarli\Feed;
4 6
7use DatePeriod;
8
5/** 9/**
6 * Simple cache system, mainly for the RSS/ATOM feeds 10 * Simple cache system, mainly for the RSS/ATOM feeds
7 */ 11 */
8class CachedPage 12class CachedPage
9{ 13{
10 // Directory containing page caches 14 /** Directory containing page caches */
11 private $cacheDir; 15 protected $cacheDir;
16
17 /** Should this URL be cached (boolean)? */
18 protected $shouldBeCached;
12 19
13 // Should this URL be cached (boolean)? 20 /** Name of the cache file for this URL */
14 private $shouldBeCached; 21 protected $filename;
15 22
16 // Name of the cache file for this URL 23 /** @var DatePeriod|null Optionally specify a period of time for cache validity */
17 private $filename; 24 protected $validityPeriod;
18 25
19 /** 26 /**
20 * Creates a new CachedPage 27 * Creates a new CachedPage
21 * 28 *
22 * @param string $cacheDir page cache directory 29 * @param string $cacheDir page cache directory
23 * @param string $url page URL 30 * @param string $url page URL
24 * @param bool $shouldBeCached whether this page needs to be cached 31 * @param bool $shouldBeCached whether this page needs to be cached
32 * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
25 */ 33 */
26 public function __construct($cacheDir, $url, $shouldBeCached) 34 public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod)
27 { 35 {
28 // TODO: check write access to the cache directory 36 // TODO: check write access to the cache directory
29 $this->cacheDir = $cacheDir; 37 $this->cacheDir = $cacheDir;
30 $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; 38 $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
31 $this->shouldBeCached = $shouldBeCached; 39 $this->shouldBeCached = $shouldBeCached;
40 $this->validityPeriod = $validityPeriod;
32 } 41 }
33 42
34 /** 43 /**
@@ -41,10 +50,20 @@ class CachedPage
41 if (!$this->shouldBeCached) { 50 if (!$this->shouldBeCached) {
42 return null; 51 return null;
43 } 52 }
44 if (is_file($this->filename)) { 53 if (!is_file($this->filename)) {
45 return file_get_contents($this->filename); 54 return null;
55 }
56 if ($this->validityPeriod !== null) {
57 $cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename));
58 if (
59 $cacheDate < $this->validityPeriod->getStartDate()
60 || $cacheDate > $this->validityPeriod->getEndDate()
61 ) {
62 return null;
63 }
46 } 64 }
47 return null; 65
66 return file_get_contents($this->filename);
48 } 67 }
49 68
50 /** 69 /**
diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php
index 4cbfcdc5..fb9cacc2 100644
--- a/application/front/controller/admin/ShaarePublishController.php
+++ b/application/front/controller/admin/ShaarePublishController.php
@@ -227,7 +227,7 @@ class ShaarePublishController extends ShaarliAdminController
227 227
228 protected function buildFormData(array $link, bool $isNew, Request $request): array 228 protected function buildFormData(array $link, bool $isNew, Request $request): array
229 { 229 {
230 $link['tags'] = strlen($link['tags']) > 0 230 $link['tags'] = $link['tags'] !== null && strlen($link['tags']) > 0
231 ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ') 231 ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ')
232 : $link['tags'] 232 : $link['tags']
233 ; 233 ;
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
index 5ae89299..29492a5f 100644
--- a/application/front/controller/visitor/DailyController.php
+++ b/application/front/controller/visitor/DailyController.php
@@ -86,9 +86,11 @@ class DailyController extends ShaarliVisitorController
86 public function rss(Request $request, Response $response): Response 86 public function rss(Request $request, Response $response): Response
87 { 87 {
88 $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); 88 $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
89 $type = DailyPageHelper::extractRequestedType($request);
90 $cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
89 91
90 $pageUrl = page_url($this->container->environment); 92 $pageUrl = page_url($this->container->environment);
91 $cache = $this->container->pageCacheManager->getCachePage($pageUrl); 93 $cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
92 94
93 $cached = $cache->cachedVersion(); 95 $cached = $cache->cachedVersion();
94 if (!empty($cached)) { 96 if (!empty($cached)) {
@@ -96,7 +98,6 @@ class DailyController extends ShaarliVisitorController
96 } 98 }
97 99
98 $days = []; 100 $days = [];
99 $type = DailyPageHelper::extractRequestedType($request);
100 $format = DailyPageHelper::getFormatByType($type); 101 $format = DailyPageHelper::getFormatByType($type);
101 $length = DailyPageHelper::getRssLengthByType($type); 102 $length = DailyPageHelper::getRssLengthByType($type);
102 foreach ($this->container->bookmarkService->search() as $bookmark) { 103 foreach ($this->container->bookmarkService->search() as $bookmark) {
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php
index 9bdb7ba5..05f95812 100644
--- a/application/helper/DailyPageHelper.php
+++ b/application/helper/DailyPageHelper.php
@@ -4,6 +4,9 @@ declare(strict_types=1);
4 4
5namespace Shaarli\Helper; 5namespace Shaarli\Helper;
6 6
7use DatePeriod;
8use DateTimeImmutable;
9use Exception;
7use Shaarli\Bookmark\Bookmark; 10use Shaarli\Bookmark\Bookmark;
8use Slim\Http\Request; 11use Slim\Http\Request;
9 12
@@ -40,31 +43,31 @@ class DailyPageHelper
40 * @param string|null $requestedDate Input string extracted from the request 43 * @param string|null $requestedDate Input string extracted from the request
41 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date) 44 * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
42 * 45 *
43 * @return \DateTimeImmutable from input or latest bookmark. 46 * @return DateTimeImmutable from input or latest bookmark.
44 * 47 *
45 * @throws \Exception Type not supported. 48 * @throws Exception Type not supported.
46 */ 49 */
47 public static function extractRequestedDateTime( 50 public static function extractRequestedDateTime(
48 string $type, 51 string $type,
49 ?string $requestedDate, 52 ?string $requestedDate,
50 Bookmark $latestBookmark = null 53 Bookmark $latestBookmark = null
51 ): \DateTimeImmutable { 54 ): DateTimeImmutable {
52 $format = static::getFormatByType($type); 55 $format = static::getFormatByType($type);
53 if (empty($requestedDate)) { 56 if (empty($requestedDate)) {
54 return $latestBookmark instanceof Bookmark 57 return $latestBookmark instanceof Bookmark
55 ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) 58 ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
56 : new \DateTimeImmutable() 59 : new DateTimeImmutable()
57 ; 60 ;
58 } 61 }
59 62
60 // W is not supported by createFromFormat... 63 // W is not supported by createFromFormat...
61 if ($type === static::WEEK) { 64 if ($type === static::WEEK) {
62 return (new \DateTimeImmutable()) 65 return (new DateTimeImmutable())
63 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) 66 ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
64 ; 67 ;
65 } 68 }
66 69
67 return \DateTimeImmutable::createFromFormat($format, $requestedDate); 70 return DateTimeImmutable::createFromFormat($format, $requestedDate);
68 } 71 }
69 72
70 /** 73 /**
@@ -80,7 +83,7 @@ class DailyPageHelper
80 * 83 *
81 * @see https://www.php.net/manual/en/datetime.format.php 84 * @see https://www.php.net/manual/en/datetime.format.php
82 * 85 *
83 * @throws \Exception Type not supported. 86 * @throws Exception Type not supported.
84 */ 87 */
85 public static function getFormatByType(string $type): string 88 public static function getFormatByType(string $type): string
86 { 89 {
@@ -92,7 +95,7 @@ class DailyPageHelper
92 case static::DAY: 95 case static::DAY:
93 return 'Ymd'; 96 return 'Ymd';
94 default: 97 default:
95 throw new \Exception('Unsupported daily format type'); 98 throw new Exception('Unsupported daily format type');
96 } 99 }
97 } 100 }
98 101
@@ -102,14 +105,14 @@ class DailyPageHelper
102 * and we don't want to alter original datetime. 105 * and we don't want to alter original datetime.
103 * 106 *
104 * @param string $type month/week/day 107 * @param string $type month/week/day
105 * @param \DateTimeImmutable $requested DateTime extracted from request input 108 * @param DateTimeImmutable $requested DateTime extracted from request input
106 * (should come from extractRequestedDateTime) 109 * (should come from extractRequestedDateTime)
107 * 110 *
108 * @return \DateTimeInterface First DateTime of the time period 111 * @return \DateTimeInterface First DateTime of the time period
109 * 112 *
110 * @throws \Exception Type not supported. 113 * @throws Exception Type not supported.
111 */ 114 */
112 public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface 115 public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
113 { 116 {
114 switch ($type) { 117 switch ($type) {
115 case static::MONTH: 118 case static::MONTH:
@@ -119,7 +122,7 @@ class DailyPageHelper
119 case static::DAY: 122 case static::DAY:
120 return $requested->modify('Today midnight'); 123 return $requested->modify('Today midnight');
121 default: 124 default:
122 throw new \Exception('Unsupported daily format type'); 125 throw new Exception('Unsupported daily format type');
123 } 126 }
124 } 127 }
125 128
@@ -129,14 +132,14 @@ class DailyPageHelper
129 * and we don't want to alter original datetime. 132 * and we don't want to alter original datetime.
130 * 133 *
131 * @param string $type month/week/day 134 * @param string $type month/week/day
132 * @param \DateTimeImmutable $requested DateTime extracted from request input 135 * @param DateTimeImmutable $requested DateTime extracted from request input
133 * (should come from extractRequestedDateTime) 136 * (should come from extractRequestedDateTime)
134 * 137 *
135 * @return \DateTimeInterface Last DateTime of the time period 138 * @return \DateTimeInterface Last DateTime of the time period
136 * 139 *
137 * @throws \Exception Type not supported. 140 * @throws Exception Type not supported.
138 */ 141 */
139 public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface 142 public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
140 { 143 {
141 switch ($type) { 144 switch ($type) {
142 case static::MONTH: 145 case static::MONTH:
@@ -146,7 +149,7 @@ class DailyPageHelper
146 case static::DAY: 149 case static::DAY:
147 return $requested->modify('Today 23:59:59'); 150 return $requested->modify('Today 23:59:59');
148 default: 151 default:
149 throw new \Exception('Unsupported daily format type'); 152 throw new Exception('Unsupported daily format type');
150 } 153 }
151 } 154 }
152 155
@@ -161,7 +164,7 @@ class DailyPageHelper
161 * 164 *
162 * @return string Localized time period description 165 * @return string Localized time period description
163 * 166 *
164 * @throws \Exception Type not supported. 167 * @throws Exception Type not supported.
165 */ 168 */
166 public static function getDescriptionByType( 169 public static function getDescriptionByType(
167 string $type, 170 string $type,
@@ -183,7 +186,7 @@ class DailyPageHelper
183 } 186 }
184 return $out . format_date($requested, false); 187 return $out . format_date($requested, false);
185 default: 188 default:
186 throw new \Exception('Unsupported daily format type'); 189 throw new Exception('Unsupported daily format type');
187 } 190 }
188 } 191 }
189 192
@@ -194,7 +197,7 @@ class DailyPageHelper
194 * 197 *
195 * @return int number of elements 198 * @return int number of elements
196 * 199 *
197 * @throws \Exception Type not supported. 200 * @throws Exception Type not supported.
198 */ 201 */
199 public static function getRssLengthByType(string $type): int 202 public static function getRssLengthByType(string $type): int
200 { 203 {
@@ -206,7 +209,28 @@ class DailyPageHelper
206 case static::DAY: 209 case static::DAY:
207 return 30; // ~1 month 210 return 30; // ~1 month
208 default: 211 default:
209 throw new \Exception('Unsupported daily format type'); 212 throw new Exception('Unsupported daily format type');
210 } 213 }
211 } 214 }
215
216 /**
217 * Get the number of items to display in the RSS feed depending on the given type.
218 *
219 * @param string $type month/week/day
220 * @param ?DateTimeImmutable $requested Currently only used for UT
221 *
222 * @return DatePeriod number of elements
223 *
224 * @throws Exception Type not supported.
225 */
226 public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod
227 {
228 $requested = $requested ?? new DateTimeImmutable();
229
230 return new DatePeriod(
231 static::getStartDateTimeByType($type, $requested),
232 new \DateInterval('P1D'),
233 static::getEndDateTimeByType($type, $requested)
234 );
235 }
212} 236}
diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php
index 2e1401ec..cfc72583 100644
--- a/application/http/MetadataRetriever.php
+++ b/application/http/MetadataRetriever.php
@@ -60,10 +60,15 @@ class MetadataRetriever
60 $title = mb_convert_encoding($title, 'utf-8', $charset); 60 $title = mb_convert_encoding($title, 'utf-8', $charset);
61 } 61 }
62 62
63 return [ 63 return array_map([$this, 'cleanMetadata'], [
64 'title' => $title, 64 'title' => $title,
65 'description' => $description, 65 'description' => $description,
66 'tags' => $tags, 66 'tags' => $tags,
67 ]; 67 ]);
68 }
69
70 protected function cleanMetadata($data): ?string
71 {
72 return !is_string($data) || empty(trim($data)) ? null : trim($data);
68 } 73 }
69} 74}
diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php
index 97805c35..fe74bf27 100644
--- a/application/render/PageCacheManager.php
+++ b/application/render/PageCacheManager.php
@@ -2,6 +2,7 @@
2 2
3namespace Shaarli\Render; 3namespace Shaarli\Render;
4 4
5use DatePeriod;
5use Shaarli\Feed\CachedPage; 6use Shaarli\Feed\CachedPage;
6 7
7/** 8/**
@@ -49,12 +50,21 @@ class PageCacheManager
49 $this->purgeCachedPages(); 50 $this->purgeCachedPages();
50 } 51 }
51 52
52 public function getCachePage(string $pageUrl): CachedPage 53 /**
54 * Get CachedPage instance for provided URL.
55 *
56 * @param string $pageUrl
57 * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
58 *
59 * @return CachedPage
60 */
61 public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage
53 { 62 {
54 return new CachedPage( 63 return new CachedPage(
55 $this->pageCacheDir, 64 $this->pageCacheDir,
56 $pageUrl, 65 $pageUrl,
57 false === $this->isLoggedIn 66 false === $this->isLoggedIn,
67 $validityPeriod
58 ); 68 );
59 } 69 }
60} 70}