aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-13 12:07:13 +0200
committerArthurHoaro <arthur@hoa.ro>2020-10-13 12:07:13 +0200
commitd9f6275ebca035fec8331652c677981056793ccc (patch)
tree37a64baf4f0eba6b781040605965383d8aded2cc /application
parent38672ba0d1c722e5d6d33a58255ceb55e9410e46 (diff)
parentd63ff87a009313141ae684ec447b902562ff6ee7 (diff)
downloadShaarli-stable.tar.gz
Shaarli-stable.tar.zst
Shaarli-stable.zip
Merge branch 'v0.11' into stablestable
Diffstat (limited to 'application')
-rw-r--r--application/ApplicationUtils.php75
-rw-r--r--application/FileUtils.php8
-rw-r--r--application/History.php12
-rw-r--r--application/Languages.php1
-rw-r--r--application/Router.php49
-rw-r--r--application/Thumbnailer.php2
-rw-r--r--application/api/ApiMiddleware.php9
-rw-r--r--application/api/ApiUtils.php8
-rw-r--r--application/api/controllers/ApiController.php7
-rw-r--r--application/api/controllers/HistoryController.php (renamed from application/api/controllers/History.php)2
-rw-r--r--application/api/controllers/Tags.php1
-rw-r--r--application/api/exceptions/ApiLinkNotFoundException.php2
-rw-r--r--application/api/exceptions/ApiTagNotFoundException.php2
-rw-r--r--application/bookmark/LinkDB.php (renamed from application/LinkDB.php)102
-rw-r--r--application/bookmark/LinkFilter.php (renamed from application/LinkFilter.php)42
-rw-r--r--application/bookmark/LinkUtils.php (renamed from application/LinkUtils.php)126
-rw-r--r--application/bookmark/exception/LinkNotFoundException.php15
-rw-r--r--application/config/ConfigJson.php2
-rw-r--r--application/config/ConfigManager.php7
-rw-r--r--application/config/ConfigPhp.php4
-rw-r--r--application/exceptions/IOException.php5
-rw-r--r--application/feed/Cache.php (renamed from application/Cache.php)0
-rw-r--r--application/feed/CachedPage.php (renamed from application/CachedPage.php)5
-rw-r--r--application/feed/FeedBuilder.php (renamed from application/FeedBuilder.php)45
-rw-r--r--application/http/Base64Url.php (renamed from application/Base64Url.php)2
-rw-r--r--application/http/HttpUtils.php (renamed from application/HttpUtils.php)5
-rw-r--r--application/http/Url.php (renamed from application/Url.php)97
-rw-r--r--application/http/UrlUtils.php88
-rw-r--r--application/netscape/NetscapeBookmarkUtils.php (renamed from application/NetscapeBookmarkUtils.php)33
-rw-r--r--application/plugin/PluginManager.php (renamed from application/PluginManager.php)39
-rw-r--r--application/plugin/exception/PluginFileNotFoundException.php23
-rw-r--r--application/render/PageBuilder.php (renamed from application/PageBuilder.php)22
-rw-r--r--application/render/ThemeUtils.php (renamed from application/ThemeUtils.php)2
-rw-r--r--application/security/BanManager.php213
-rw-r--r--application/security/LoginManager.php103
-rw-r--r--application/updater/Updater.php (renamed from application/Updater.php)139
-rw-r--r--application/updater/UpdaterUtils.php39
-rw-r--r--application/updater/exception/UpdaterException.php60
38 files changed, 845 insertions, 551 deletions
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 @@
1<?php 1<?php
2namespace Shaarli;
3
4use Exception;
5use Shaarli\Config\ConfigManager;
6
2/** 7/**
3 * Shaarli (application) utilities 8 * Shaarli (application) utilities
4 */ 9 */
@@ -51,7 +56,7 @@ class ApplicationUtils
51 return false; 56 return false;
52 } 57 }
53 } else { 58 } else {
54 if (! is_file($remote)) { 59 if (!is_file($remote)) {
55 return false; 60 return false;
56 } 61 }
57 $data = file_get_contents($remote); 62 $data = file_get_contents($remote);
@@ -97,7 +102,7 @@ class ApplicationUtils
97 // Do not check versions for visitors 102 // Do not check versions for visitors
98 // Do not check if the user doesn't want to 103 // Do not check if the user doesn't want to
99 // Do not check with dev version 104 // Do not check with dev version
100 if (! $isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') { 105 if (!$isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') {
101 return false; 106 return false;
102 } 107 }
103 108
@@ -111,7 +116,7 @@ class ApplicationUtils
111 return false; 116 return false;
112 } 117 }
113 118
114 if (! in_array($branch, self::$GIT_BRANCHES)) { 119 if (!in_array($branch, self::$GIT_BRANCHES)) {
115 throw new Exception( 120 throw new Exception(
116 'Invalid branch selected for updates: "' . $branch . '"' 121 'Invalid branch selected for updates: "' . $branch . '"'
117 ); 122 );
@@ -123,7 +128,7 @@ class ApplicationUtils
123 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE 128 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE
124 ); 129 );
125 130
126 if (! $latestVersion) { 131 if (!$latestVersion) {
127 // Only update the file's modification date 132 // Only update the file's modification date
128 file_put_contents($updateFile, $currentVersion); 133 file_put_contents($updateFile, $currentVersion);
129 return false; 134 return false;
@@ -152,9 +157,9 @@ class ApplicationUtils
152 if (version_compare($curVersion, $minVersion) < 0) { 157 if (version_compare($curVersion, $minVersion) < 0) {
153 $msg = t( 158 $msg = t(
154 'Your PHP version is obsolete!' 159 'Your PHP version is obsolete!'
155 . ' Shaarli requires at least PHP %s, and thus cannot run.' 160 . ' Shaarli requires at least PHP %s, and thus cannot run.'
156 . ' Your PHP version has known security vulnerabilities and should be' 161 . ' Your PHP version has known security vulnerabilities and should be'
157 . ' updated as soon as possible.' 162 . ' updated as soon as possible.'
158 ); 163 );
159 throw new Exception(sprintf($msg, $minVersion)); 164 throw new Exception(sprintf($msg, $minVersion));
160 } 165 }
@@ -174,50 +179,50 @@ class ApplicationUtils
174 179
175 // Check script and template directories are readable 180 // Check script and template directories are readable
176 foreach (array( 181 foreach (array(
177 'application', 182 'application',
178 'inc', 183 'inc',
179 'plugins', 184 'plugins',
180 $rainTplDir, 185 $rainTplDir,
181 $rainTplDir.'/'.$conf->get('resource.theme'), 186 $rainTplDir . '/' . $conf->get('resource.theme'),
182 ) as $path) { 187 ) as $path) {
183 if (! is_readable(realpath($path))) { 188 if (!is_readable(realpath($path))) {
184 $errors[] = '"'.$path.'" '. t('directory is not readable'); 189 $errors[] = '"' . $path . '" ' . t('directory is not readable');
185 } 190 }
186 } 191 }
187 192
188 // Check cache and data directories are readable and writable 193 // Check cache and data directories are readable and writable
189 foreach (array( 194 foreach (array(
190 $conf->get('resource.thumbnails_cache'), 195 $conf->get('resource.thumbnails_cache'),
191 $conf->get('resource.data_dir'), 196 $conf->get('resource.data_dir'),
192 $conf->get('resource.page_cache'), 197 $conf->get('resource.page_cache'),
193 $conf->get('resource.raintpl_tmp'), 198 $conf->get('resource.raintpl_tmp'),
194 ) as $path) { 199 ) as $path) {
195 if (! is_readable(realpath($path))) { 200 if (!is_readable(realpath($path))) {
196 $errors[] = '"'.$path.'" '. t('directory is not readable'); 201 $errors[] = '"' . $path . '" ' . t('directory is not readable');
197 } 202 }
198 if (! is_writable(realpath($path))) { 203 if (!is_writable(realpath($path))) {
199 $errors[] = '"'.$path.'" '. t('directory is not writable'); 204 $errors[] = '"' . $path . '" ' . t('directory is not writable');
200 } 205 }
201 } 206 }
202 207
203 // Check configuration files are readable and writable 208 // Check configuration files are readable and writable
204 foreach (array( 209 foreach (array(
205 $conf->getConfigFileExt(), 210 $conf->getConfigFileExt(),
206 $conf->get('resource.datastore'), 211 $conf->get('resource.datastore'),
207 $conf->get('resource.ban_file'), 212 $conf->get('resource.ban_file'),
208 $conf->get('resource.log'), 213 $conf->get('resource.log'),
209 $conf->get('resource.update_check'), 214 $conf->get('resource.update_check'),
210 ) as $path) { 215 ) as $path) {
211 if (! is_file(realpath($path))) { 216 if (!is_file(realpath($path))) {
212 # the file may not exist yet 217 # the file may not exist yet
213 continue; 218 continue;
214 } 219 }
215 220
216 if (! is_readable(realpath($path))) { 221 if (!is_readable(realpath($path))) {
217 $errors[] = '"'.$path.'" '. t('file is not readable'); 222 $errors[] = '"' . $path . '" ' . t('file is not readable');
218 } 223 }
219 if (! is_writable(realpath($path))) { 224 if (!is_writable(realpath($path))) {
220 $errors[] = '"'.$path.'" '. t('file is not writable'); 225 $errors[] = '"' . $path . '" ' . t('file is not writable');
221 } 226 }
222 } 227 }
223 228
diff --git a/application/FileUtils.php b/application/FileUtils.php
index b89ea12b..30560bfc 100644
--- a/application/FileUtils.php
+++ b/application/FileUtils.php
@@ -1,6 +1,8 @@
1<?php 1<?php
2 2
3require_once 'exceptions/IOException.php'; 3namespace Shaarli;
4
5use Shaarli\Exceptions\IOException;
4 6
5/** 7/**
6 * Class FileUtils 8 * Class FileUtils
@@ -44,7 +46,7 @@ class FileUtils
44 46
45 return file_put_contents( 47 return file_put_contents(
46 $file, 48 $file,
47 self::$phpPrefix.base64_encode(gzdeflate(serialize($content))).self::$phpSuffix 49 self::$phpPrefix . base64_encode(gzdeflate(serialize($content))) . self::$phpSuffix
48 ); 50 );
49 } 51 }
50 52
@@ -62,7 +64,7 @@ class FileUtils
62 { 64 {
63 // Note that gzinflate is faster than gzuncompress. 65 // Note that gzinflate is faster than gzuncompress.
64 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 66 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
65 if (! is_readable($file)) { 67 if (!is_readable($file)) {
66 return $default; 68 return $default;
67 } 69 }
68 70
diff --git a/application/History.php b/application/History.php
index 35ec016a..a5846652 100644
--- a/application/History.php
+++ b/application/History.php
@@ -1,4 +1,8 @@
1<?php 1<?php
2namespace Shaarli;
3
4use DateTime;
5use Exception;
2 6
3/** 7/**
4 * Class History 8 * Class History
@@ -66,7 +70,7 @@ class History
66 * History constructor. 70 * History constructor.
67 * 71 *
68 * @param string $historyFilePath History file path. 72 * @param string $historyFilePath History file path.
69 * @param int $retentionTime History content rentention time in seconds. 73 * @param int $retentionTime History content retention time in seconds.
70 * 74 *
71 * @throws Exception if something goes wrong. 75 * @throws Exception if something goes wrong.
72 */ 76 */
@@ -166,11 +170,11 @@ class History
166 */ 170 */
167 protected function check() 171 protected function check()
168 { 172 {
169 if (! is_file($this->historyFilePath)) { 173 if (!is_file($this->historyFilePath)) {
170 FileUtils::writeFlatDB($this->historyFilePath, []); 174 FileUtils::writeFlatDB($this->historyFilePath, []);
171 } 175 }
172 176
173 if (! is_writable($this->historyFilePath)) { 177 if (!is_writable($this->historyFilePath)) {
174 throw new Exception(t('History file isn\'t readable or writable')); 178 throw new Exception(t('History file isn\'t readable or writable'));
175 } 179 }
176 } 180 }
@@ -191,7 +195,7 @@ class History
191 */ 195 */
192 protected function write() 196 protected function write()
193 { 197 {
194 $comparaison = new DateTime('-'. $this->retentionTime . ' seconds'); 198 $comparaison = new DateTime('-' . $this->retentionTime . ' seconds');
195 foreach ($this->history as $key => $value) { 199 foreach ($this->history as $key => $value) {
196 if ($value['datetime'] < $comparaison) { 200 if ($value['datetime'] < $comparaison) {
197 unset($this->history[$key]); 201 unset($this->history[$key]);
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 @@
3namespace Shaarli; 3namespace Shaarli;
4 4
5use Gettext\GettextTranslator; 5use Gettext\GettextTranslator;
6use Gettext\Merge;
7use Gettext\Translations; 6use Gettext\Translations;
8use Gettext\Translator; 7use Gettext\Translator;
9use Gettext\TranslatorInterface; 8use Gettext\TranslatorInterface;
diff --git a/application/Router.php b/application/Router.php
index beb3165b..d7187487 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -1,4 +1,5 @@
1<?php 1<?php
2namespace Shaarli;
2 3
3/** 4/**
4 * Class Router 5 * Class Router
@@ -37,6 +38,8 @@ class Router
37 38
38 public static $PAGE_DELETELINK = 'delete_link'; 39 public static $PAGE_DELETELINK = 'delete_link';
39 40
41 public static $PAGE_CHANGE_VISIBILITY = 'change_visibility';
42
40 public static $PAGE_PINLINK = 'pin'; 43 public static $PAGE_PINLINK = 'pin';
41 44
42 public static $PAGE_EXPORT = 'export'; 45 public static $PAGE_EXPORT = 'export';
@@ -75,43 +78,43 @@ class Router
75 return self::$PAGE_LINKLIST; 78 return self::$PAGE_LINKLIST;
76 } 79 }
77 80
78 if (startsWith($query, 'do='. self::$PAGE_LOGIN) && $loggedIn === false) { 81 if (startsWith($query, 'do=' . self::$PAGE_LOGIN) && $loggedIn === false) {
79 return self::$PAGE_LOGIN; 82 return self::$PAGE_LOGIN;
80 } 83 }
81 84
82 if (startsWith($query, 'do='. self::$PAGE_PICWALL)) { 85 if (startsWith($query, 'do=' . self::$PAGE_PICWALL)) {
83 return self::$PAGE_PICWALL; 86 return self::$PAGE_PICWALL;
84 } 87 }
85 88
86 if (startsWith($query, 'do='. self::$PAGE_TAGCLOUD)) { 89 if (startsWith($query, 'do=' . self::$PAGE_TAGCLOUD)) {
87 return self::$PAGE_TAGCLOUD; 90 return self::$PAGE_TAGCLOUD;
88 } 91 }
89 92
90 if (startsWith($query, 'do='. self::$PAGE_TAGLIST)) { 93 if (startsWith($query, 'do=' . self::$PAGE_TAGLIST)) {
91 return self::$PAGE_TAGLIST; 94 return self::$PAGE_TAGLIST;
92 } 95 }
93 96
94 if (startsWith($query, 'do='. self::$PAGE_OPENSEARCH)) { 97 if (startsWith($query, 'do=' . self::$PAGE_OPENSEARCH)) {
95 return self::$PAGE_OPENSEARCH; 98 return self::$PAGE_OPENSEARCH;
96 } 99 }
97 100
98 if (startsWith($query, 'do='. self::$PAGE_DAILY)) { 101 if (startsWith($query, 'do=' . self::$PAGE_DAILY)) {
99 return self::$PAGE_DAILY; 102 return self::$PAGE_DAILY;
100 } 103 }
101 104
102 if (startsWith($query, 'do='. self::$PAGE_FEED_ATOM)) { 105 if (startsWith($query, 'do=' . self::$PAGE_FEED_ATOM)) {
103 return self::$PAGE_FEED_ATOM; 106 return self::$PAGE_FEED_ATOM;
104 } 107 }
105 108
106 if (startsWith($query, 'do='. self::$PAGE_FEED_RSS)) { 109 if (startsWith($query, 'do=' . self::$PAGE_FEED_RSS)) {
107 return self::$PAGE_FEED_RSS; 110 return self::$PAGE_FEED_RSS;
108 } 111 }
109 112
110 if (startsWith($query, 'do='. self::$PAGE_THUMBS_UPDATE)) { 113 if (startsWith($query, 'do=' . self::$PAGE_THUMBS_UPDATE)) {
111 return self::$PAGE_THUMBS_UPDATE; 114 return self::$PAGE_THUMBS_UPDATE;
112 } 115 }
113 116
114 if (startsWith($query, 'do='. self::$AJAX_THUMB_UPDATE)) { 117 if (startsWith($query, 'do=' . self::$AJAX_THUMB_UPDATE)) {
115 return self::$AJAX_THUMB_UPDATE; 118 return self::$AJAX_THUMB_UPDATE;
116 } 119 }
117 120
@@ -120,23 +123,23 @@ class Router
120 return self::$PAGE_LINKLIST; 123 return self::$PAGE_LINKLIST;
121 } 124 }
122 125
123 if (startsWith($query, 'do='. self::$PAGE_TOOLS)) { 126 if (startsWith($query, 'do=' . self::$PAGE_TOOLS)) {
124 return self::$PAGE_TOOLS; 127 return self::$PAGE_TOOLS;
125 } 128 }
126 129
127 if (startsWith($query, 'do='. self::$PAGE_CHANGEPASSWORD)) { 130 if (startsWith($query, 'do=' . self::$PAGE_CHANGEPASSWORD)) {
128 return self::$PAGE_CHANGEPASSWORD; 131 return self::$PAGE_CHANGEPASSWORD;
129 } 132 }
130 133
131 if (startsWith($query, 'do='. self::$PAGE_CONFIGURE)) { 134 if (startsWith($query, 'do=' . self::$PAGE_CONFIGURE)) {
132 return self::$PAGE_CONFIGURE; 135 return self::$PAGE_CONFIGURE;
133 } 136 }
134 137
135 if (startsWith($query, 'do='. self::$PAGE_CHANGETAG)) { 138 if (startsWith($query, 'do=' . self::$PAGE_CHANGETAG)) {
136 return self::$PAGE_CHANGETAG; 139 return self::$PAGE_CHANGETAG;
137 } 140 }
138 141
139 if (startsWith($query, 'do='. self::$PAGE_ADDLINK)) { 142 if (startsWith($query, 'do=' . self::$PAGE_ADDLINK)) {
140 return self::$PAGE_ADDLINK; 143 return self::$PAGE_ADDLINK;
141 } 144 }
142 145
@@ -148,27 +151,31 @@ class Router
148 return self::$PAGE_DELETELINK; 151 return self::$PAGE_DELETELINK;
149 } 152 }
150 153
151 if (startsWith($query, 'do='. self::$PAGE_PINLINK)) { 154 if (isset($get[self::$PAGE_CHANGE_VISIBILITY])) {
155 return self::$PAGE_CHANGE_VISIBILITY;
156 }
157
158 if (startsWith($query, 'do=' . self::$PAGE_PINLINK)) {
152 return self::$PAGE_PINLINK; 159 return self::$PAGE_PINLINK;
153 } 160 }
154 161
155 if (startsWith($query, 'do='. self::$PAGE_EXPORT)) { 162 if (startsWith($query, 'do=' . self::$PAGE_EXPORT)) {
156 return self::$PAGE_EXPORT; 163 return self::$PAGE_EXPORT;
157 } 164 }
158 165
159 if (startsWith($query, 'do='. self::$PAGE_IMPORT)) { 166 if (startsWith($query, 'do=' . self::$PAGE_IMPORT)) {
160 return self::$PAGE_IMPORT; 167 return self::$PAGE_IMPORT;
161 } 168 }
162 169
163 if (startsWith($query, 'do='. self::$PAGE_PLUGINSADMIN)) { 170 if (startsWith($query, 'do=' . self::$PAGE_PLUGINSADMIN)) {
164 return self::$PAGE_PLUGINSADMIN; 171 return self::$PAGE_PLUGINSADMIN;
165 } 172 }
166 173
167 if (startsWith($query, 'do='. self::$PAGE_SAVE_PLUGINSADMIN)) { 174 if (startsWith($query, 'do=' . self::$PAGE_SAVE_PLUGINSADMIN)) {
168 return self::$PAGE_SAVE_PLUGINSADMIN; 175 return self::$PAGE_SAVE_PLUGINSADMIN;
169 } 176 }
170 177
171 if (startsWith($query, 'do='. self::$GET_TOKEN)) { 178 if (startsWith($query, 'do=' . self::$GET_TOKEN)) {
172 return self::$GET_TOKEN; 179 return self::$GET_TOKEN;
173 } 180 }
174 181
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
index 167d6296..d5f5ac28 100644
--- a/application/Thumbnailer.php
+++ b/application/Thumbnailer.php
@@ -3,9 +3,9 @@
3namespace Shaarli; 3namespace Shaarli;
4 4
5use Shaarli\Config\ConfigManager; 5use Shaarli\Config\ConfigManager;
6use WebThumbnailer\Application\ConfigManager as WTConfigManager;
6use WebThumbnailer\Exception\WebThumbnailerException; 7use WebThumbnailer\Exception\WebThumbnailerException;
7use WebThumbnailer\WebThumbnailer; 8use WebThumbnailer\WebThumbnailer;
8use WebThumbnailer\Application\ConfigManager as WTConfigManager;
9 9
10/** 10/**
11 * Class Thumbnailer 11 * Class Thumbnailer
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 66eac133..2d55bda6 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -1,9 +1,8 @@
1<?php 1<?php
2namespace Shaarli\Api; 2namespace Shaarli\Api;
3 3
4use Shaarli\Api\Exceptions\ApiException;
5use Shaarli\Api\Exceptions\ApiAuthorizationException; 4use Shaarli\Api\Exceptions\ApiAuthorizationException;
6 5use Shaarli\Api\Exceptions\ApiException;
7use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
8use Slim\Container; 7use Slim\Container;
9use Slim\Http\Request; 8use Slim\Http\Request;
@@ -127,12 +126,10 @@ class ApiMiddleware
127 */ 126 */
128 protected function setLinkDb($conf) 127 protected function setLinkDb($conf)
129 { 128 {
130 $linkDb = new \LinkDB( 129 $linkDb = new \Shaarli\Bookmark\LinkDB(
131 $conf->get('resource.datastore'), 130 $conf->get('resource.datastore'),
132 true, 131 true,
133 $conf->get('privacy.hide_public_links'), 132 $conf->get('privacy.hide_public_links')
134 $conf->get('redirector.url'),
135 $conf->get('redirector.encode_url')
136 ); 133 );
137 $this->container['db'] = $linkDb; 134 $this->container['db'] = $linkDb;
138 } 135 }
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index fc5ecaf1..1e3ac02e 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -1,8 +1,8 @@
1<?php 1<?php
2namespace Shaarli\Api; 2namespace Shaarli\Api;
3 3
4use Shaarli\Base64Url;
5use Shaarli\Api\Exceptions\ApiAuthorizationException; 4use Shaarli\Api\Exceptions\ApiAuthorizationException;
5use Shaarli\Http\Base64Url;
6 6
7/** 7/**
8 * REST API utilities 8 * REST API utilities
@@ -12,7 +12,7 @@ class ApiUtils
12 /** 12 /**
13 * Validates a JWT token authenticity. 13 * Validates a JWT token authenticity.
14 * 14 *
15 * @param string $token JWT token extracted from the headers. 15 * @param string $token JWT token extracted from the headers.
16 * @param string $secret API secret set in the settings. 16 * @param string $secret API secret set in the settings.
17 * 17 *
18 * @throws ApiAuthorizationException the token is not valid. 18 * @throws ApiAuthorizationException the token is not valid.
@@ -50,7 +50,7 @@ class ApiUtils
50 /** 50 /**
51 * Format a Link for the REST API. 51 * Format a Link for the REST API.
52 * 52 *
53 * @param array $link Link data read from the datastore. 53 * @param array $link Link data read from the datastore.
54 * @param string $indexUrl Shaarli's index URL (used for relative URL). 54 * @param string $indexUrl Shaarli's index URL (used for relative URL).
55 * 55 *
56 * @return array Link data formatted for the REST API. 56 * @return array Link data formatted for the REST API.
@@ -59,7 +59,7 @@ class ApiUtils
59 { 59 {
60 $out['id'] = $link['id']; 60 $out['id'] = $link['id'];
61 // Not an internal link 61 // Not an internal link
62 if ($link['url'][0] != '?') { 62 if (! is_note($link['url'])) {
63 $out['url'] = $link['url']; 63 $out['url'] = $link['url'];
64 } else { 64 } else {
65 $out['url'] = $indexUrl . $link['url']; 65 $out['url'] = $indexUrl . $link['url'];
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index 9edefcf6..a6e7cbab 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -2,8 +2,9 @@
2 2
3namespace Shaarli\Api\Controllers; 3namespace Shaarli\Api\Controllers;
4 4
5use Shaarli\Bookmark\LinkDB;
5use Shaarli\Config\ConfigManager; 6use Shaarli\Config\ConfigManager;
6use \Slim\Container; 7use Slim\Container;
7 8
8/** 9/**
9 * Abstract Class ApiController 10 * Abstract Class ApiController
@@ -25,12 +26,12 @@ abstract class ApiController
25 protected $conf; 26 protected $conf;
26 27
27 /** 28 /**
28 * @var \LinkDB 29 * @var LinkDB
29 */ 30 */
30 protected $linkDb; 31 protected $linkDb;
31 32
32 /** 33 /**
33 * @var \History 34 * @var HistoryController
34 */ 35 */
35 protected $history; 36 protected $history;
36 37
diff --git a/application/api/controllers/History.php b/application/api/controllers/HistoryController.php
index 4582e8b2..9afcfa26 100644
--- a/application/api/controllers/History.php
+++ b/application/api/controllers/HistoryController.php
@@ -14,7 +14,7 @@ use Slim\Http\Response;
14 * 14 *
15 * @package Shaarli\Api\Controllers 15 * @package Shaarli\Api\Controllers
16 */ 16 */
17class History extends ApiController 17class HistoryController extends ApiController
18{ 18{
19 /** 19 /**
20 * Service providing operation regarding Shaarli datastore and settings. 20 * Service providing operation regarding Shaarli datastore and settings.
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;
4 4
5use Shaarli\Api\ApiUtils; 5use Shaarli\Api\ApiUtils;
6use Shaarli\Api\Exceptions\ApiBadParametersException; 6use Shaarli\Api\Exceptions\ApiBadParametersException;
7use Shaarli\Api\Exceptions\ApiLinkNotFoundException;
8use Shaarli\Api\Exceptions\ApiTagNotFoundException; 7use Shaarli\Api\Exceptions\ApiTagNotFoundException;
9use Slim\Http\Request; 8use Slim\Http\Request;
10use Slim\Http\Response; 9use 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 @@
2 2
3namespace Shaarli\Api\Exceptions; 3namespace Shaarli\Api\Exceptions;
4 4
5use Slim\Http\Response;
6
7/** 5/**
8 * Class ApiLinkNotFoundException 6 * Class ApiLinkNotFoundException
9 * 7 *
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 @@
2 2
3namespace Shaarli\Api\Exceptions; 3namespace Shaarli\Api\Exceptions;
4 4
5use Slim\Http\Response;
6
7/** 5/**
8 * Class ApiTagNotFoundException 6 * Class ApiTagNotFoundException
9 * 7 *
diff --git a/application/LinkDB.php b/application/bookmark/LinkDB.php
index 803757ca..76ba95f0 100644
--- a/application/LinkDB.php
+++ b/application/bookmark/LinkDB.php
@@ -1,4 +1,15 @@
1<?php 1<?php
2
3namespace Shaarli\Bookmark;
4
5use ArrayAccess;
6use Countable;
7use DateTime;
8use Iterator;
9use Shaarli\Bookmark\Exception\LinkNotFoundException;
10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils;
12
2/** 13/**
3 * Data storage for links. 14 * Data storage for links.
4 * 15 *
@@ -18,10 +29,10 @@
18 * - private: Is this link private? 0=no, other value=yes 29 * - private: Is this link private? 0=no, other value=yes
19 * - tags: tags attached to this entry (separated by spaces) 30 * - tags: tags attached to this entry (separated by spaces)
20 * - title Title of the link 31 * - title Title of the link
21 * - url URL of the link. Used for displayable links (no redirector, relative, etc.). 32 * - url URL of the link. Used for displayable links.
22 * Can be absolute or relative. 33 * Can be absolute or relative in the database but the relative links
23 * Relative URLs are permalinks (e.g.'?m-ukcw') 34 * will be converted to absolute ones in templates.
24 * - real_url Absolute processed URL. 35 * - real_url Raw URL in stored in the DB (absolute or relative).
25 * - shorturl Permalink smallhash 36 * - shorturl Permalink smallhash
26 * 37 *
27 * Implements 3 interfaces: 38 * Implements 3 interfaces:
@@ -77,19 +88,6 @@ class LinkDB implements Iterator, Countable, ArrayAccess
77 // Hide public links 88 // Hide public links
78 private $hidePublicLinks; 89 private $hidePublicLinks;
79 90
80 // link redirector set in user settings.
81 private $redirector;
82
83 /**
84 * Set this to `true` to urlencode link behind redirector link, `false` to leave it untouched.
85 *
86 * Example:
87 * anonym.to needs clean URL while dereferer.org needs urlencoded URL.
88 *
89 * @var boolean $redirectorEncode parameter: true or false
90 */
91 private $redirectorEncode;
92
93 /** 91 /**
94 * Creates a new LinkDB 92 * Creates a new LinkDB
95 * 93 *
@@ -98,21 +96,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
98 * @param string $datastore datastore file path. 96 * @param string $datastore datastore file path.
99 * @param boolean $isLoggedIn is the user logged in? 97 * @param boolean $isLoggedIn is the user logged in?
100 * @param boolean $hidePublicLinks if true all links are private. 98 * @param boolean $hidePublicLinks if true all links are private.
101 * @param string $redirector link redirector set in user settings.
102 * @param boolean $redirectorEncode Enable urlencode on redirected urls (default: true).
103 */ 99 */
104 public function __construct( 100 public function __construct(
105 $datastore, 101 $datastore,
106 $isLoggedIn, 102 $isLoggedIn,
107 $hidePublicLinks, 103 $hidePublicLinks
108 $redirector = '',
109 $redirectorEncode = true
110 ) { 104 ) {
105
111 $this->datastore = $datastore; 106 $this->datastore = $datastore;
112 $this->loggedIn = $isLoggedIn; 107 $this->loggedIn = $isLoggedIn;
113 $this->hidePublicLinks = $hidePublicLinks; 108 $this->hidePublicLinks = $hidePublicLinks;
114 $this->redirector = $redirector;
115 $this->redirectorEncode = $redirectorEncode === true;
116 $this->check(); 109 $this->check();
117 $this->read(); 110 $this->read();
118 } 111 }
@@ -137,7 +130,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
137 if (!isset($value['id']) || empty($value['url'])) { 130 if (!isset($value['id']) || empty($value['url'])) {
138 die(t('Internal Error: A link should always have an id and URL.')); 131 die(t('Internal Error: A link should always have an id and URL.'));
139 } 132 }
140 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { 133 if (($offset !== null && !is_int($offset)) || !is_int($value['id'])) {
141 die(t('You must specify an integer as a key.')); 134 die(t('You must specify an integer as a key.'));
142 } 135 }
143 if ($offset !== null && $offset !== $value['id']) { 136 if ($offset !== null && $offset !== $value['id']) {
@@ -247,19 +240,19 @@ class LinkDB implements Iterator, Countable, ArrayAccess
247 $this->links = array(); 240 $this->links = array();
248 $link = array( 241 $link = array(
249 'id' => 1, 242 'id' => 1,
250 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'), 243 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'),
251 'url'=>'https://shaarli.readthedocs.io', 244 'url' => 'https://shaarli.readthedocs.io',
252 'description'=>t( 245 'description' => t(
253 'Welcome to Shaarli! This is your first public bookmark. ' 246 'Welcome to Shaarli! This is your first public bookmark. '
254 .'To edit or delete me, you must first login. 247 . 'To edit or delete me, you must first login.
255 248
256To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. 249To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
257 250
258You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' 251You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
259 ), 252 ),
260 'private'=>0, 253 'private' => 0,
261 'created'=> new DateTime(), 254 'created' => new DateTime(),
262 'tags'=>'opensource software', 255 'tags' => 'opensource software',
263 'sticky' => false, 256 'sticky' => false,
264 ); 257 );
265 $link['shorturl'] = link_small_hash($link['created'], $link['id']); 258 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
@@ -267,12 +260,12 @@ You use the community supported version of the original Shaarli project, by Seba
267 260
268 $link = array( 261 $link = array(
269 'id' => 0, 262 'id' => 0,
270 'title'=> t('My secret stuff... - Pastebin.com'), 263 'title' => t('My secret stuff... - Pastebin.com'),
271 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 264 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
272 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'), 265 'description' => t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
273 'private'=>1, 266 'private' => 1,
274 'created'=> new DateTime('1 minute ago'), 267 'created' => new DateTime('1 minute ago'),
275 'tags'=>'secretstuff', 268 'tags' => 'secretstuff',
276 'sticky' => false, 269 'sticky' => false,
277 ); 270 );
278 $link['shorturl'] = link_small_hash($link['created'], $link['id']); 271 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
@@ -299,7 +292,7 @@ You use the community supported version of the original Shaarli project, by Seba
299 292
300 $toremove = array(); 293 $toremove = array();
301 foreach ($this->links as $key => &$link) { 294 foreach ($this->links as $key => &$link) {
302 if (! $this->loggedIn && $link['private'] != 0) { 295 if (!$this->loggedIn && $link['private'] != 0) {
303 // Transition for not upgraded databases. 296 // Transition for not upgraded databases.
304 unset($this->links[$key]); 297 unset($this->links[$key]);
305 continue; 298 continue;
@@ -309,29 +302,19 @@ You use the community supported version of the original Shaarli project, by Seba
309 sanitizeLink($link); 302 sanitizeLink($link);
310 303
311 // Remove private tags if the user is not logged in. 304 // Remove private tags if the user is not logged in.
312 if (! $this->loggedIn) { 305 if (!$this->loggedIn) {
313 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']); 306 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
314 } 307 }
315 308
316 // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). 309 $link['real_url'] = $link['url'];
317 if (!empty($this->redirector) && !startsWith($link['url'], '?')) {
318 $link['real_url'] = $this->redirector;
319 if ($this->redirectorEncode) {
320 $link['real_url'] .= urlencode(unescape($link['url']));
321 } else {
322 $link['real_url'] .= $link['url'];
323 }
324 } else {
325 $link['real_url'] = $link['url'];
326 }
327 310
328 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false; 311 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
329 312
330 // To be able to load links before running the update, and prepare the update 313 // To be able to load links before running the update, and prepare the update
331 if (! isset($link['created'])) { 314 if (!isset($link['created'])) {
332 $link['id'] = $link['linkdate']; 315 $link['id'] = $link['linkdate'];
333 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']); 316 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
334 if (! empty($link['updated'])) { 317 if (!empty($link['updated'])) {
335 $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']); 318 $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
336 } 319 }
337 $link['shorturl'] = smallHash($link['linkdate']); 320 $link['shorturl'] = smallHash($link['linkdate']);
@@ -417,12 +400,12 @@ You use the community supported version of the original Shaarli project, by Seba
417 /** 400 /**
418 * Filter links according to search parameters. 401 * Filter links according to search parameters.
419 * 402 *
420 * @param array $filterRequest Search request content. Supported keys: 403 * @param array $filterRequest Search request content. Supported keys:
421 * - searchtags: list of tags 404 * - searchtags: list of tags
422 * - searchterm: term search 405 * - searchterm: term search
423 * @param bool $casesensitive Optional: Perform case sensitive filter 406 * @param bool $casesensitive Optional: Perform case sensitive filter
424 * @param string $visibility return only all/private/public links 407 * @param string $visibility return only all/private/public links
425 * @param string $untaggedonly return only untagged links 408 * @param bool $untaggedonly return only untagged links
426 * 409 *
427 * @return array filtered links, all links if no suitable filter was provided. 410 * @return array filtered links, all links if no suitable filter was provided.
428 */ 411 */
@@ -432,6 +415,7 @@ You use the community supported version of the original Shaarli project, by Seba
432 $visibility = 'all', 415 $visibility = 'all',
433 $untaggedonly = false 416 $untaggedonly = false
434 ) { 417 ) {
418
435 // Filter link database according to parameters. 419 // Filter link database according to parameters.
436 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : ''; 420 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
437 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : ''; 421 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
@@ -447,8 +431,8 @@ You use the community supported version of the original Shaarli project, by Seba
447 /** 431 /**
448 * Returns the list tags appearing in the links with the given tags 432 * Returns the list tags appearing in the links with the given tags
449 * 433 *
450 * @param array $filteringTags tags selecting the links to consider 434 * @param array $filteringTags tags selecting the links to consider
451 * @param string $visibility process only all/private/public links 435 * @param string $visibility process only all/private/public links
452 * 436 *
453 * @return array tag => linksCount 437 * @return array tag => linksCount
454 */ 438 */
diff --git a/application/LinkFilter.php b/application/bookmark/LinkFilter.php
index 8f147974..9b966307 100644
--- a/application/LinkFilter.php
+++ b/application/bookmark/LinkFilter.php
@@ -1,5 +1,10 @@
1<?php 1<?php
2 2
3namespace Shaarli\Bookmark;
4
5use Exception;
6use Shaarli\Bookmark\Exception\LinkNotFoundException;
7
3/** 8/**
4 * Class LinkFilter. 9 * Class LinkFilter.
5 * 10 *
@@ -10,22 +15,22 @@ class LinkFilter
10 /** 15 /**
11 * @var string permalinks. 16 * @var string permalinks.
12 */ 17 */
13 public static $FILTER_HASH = 'permalink'; 18 public static $FILTER_HASH = 'permalink';
14 19
15 /** 20 /**
16 * @var string text search. 21 * @var string text search.
17 */ 22 */
18 public static $FILTER_TEXT = 'fulltext'; 23 public static $FILTER_TEXT = 'fulltext';
19 24
20 /** 25 /**
21 * @var string tag filter. 26 * @var string tag filter.
22 */ 27 */
23 public static $FILTER_TAG = 'tags'; 28 public static $FILTER_TAG = 'tags';
24 29
25 /** 30 /**
26 * @var string filter by day. 31 * @var string filter by day.
27 */ 32 */
28 public static $FILTER_DAY = 'FILTER_DAY'; 33 public static $FILTER_DAY = 'FILTER_DAY';
29 34
30 /** 35 /**
31 * @var string Allowed characters for hashtags (regex syntax). 36 * @var string Allowed characters for hashtags (regex syntax).
@@ -58,7 +63,7 @@ class LinkFilter
58 */ 63 */
59 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false) 64 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false)
60 { 65 {
61 if (! in_array($visibility, ['all', 'public', 'private'])) { 66 if (!in_array($visibility, ['all', 'public', 'private'])) {
62 $visibility = 'all'; 67 $visibility = 'all';
63 } 68 }
64 69
@@ -117,7 +122,7 @@ class LinkFilter
117 foreach ($this->links as $key => $value) { 122 foreach ($this->links as $key => $value) {
118 if ($value['private'] && $visibility === 'private') { 123 if ($value['private'] && $visibility === 'private') {
119 $out[$key] = $value; 124 $out[$key] = $value;
120 } elseif (! $value['private'] && $visibility === 'public') { 125 } elseif (!$value['private'] && $visibility === 'public') {
121 $out[$key] = $value; 126 $out[$key] = $value;
122 } 127 }
123 } 128 }
@@ -132,7 +137,7 @@ class LinkFilter
132 * 137 *
133 * @return array $filtered array containing permalink data. 138 * @return array $filtered array containing permalink data.
134 * 139 *
135 * @throws LinkNotFoundException if the smallhash doesn't match any link. 140 * @throws \Shaarli\Bookmark\Exception\LinkNotFoundException if the smallhash doesn't match any link.
136 */ 141 */
137 private function filterSmallHash($smallHash) 142 private function filterSmallHash($smallHash)
138 { 143 {
@@ -169,7 +174,7 @@ class LinkFilter
169 * - see https://github.com/shaarli/Shaarli/issues/75 for examples 174 * - see https://github.com/shaarli/Shaarli/issues/75 for examples
170 * 175 *
171 * @param string $searchterms search query. 176 * @param string $searchterms search query.
172 * @param string $visibility Optional: return only all/private/public links. 177 * @param string $visibility Optional: return only all/private/public links.
173 * 178 *
174 * @return array search results. 179 * @return array search results.
175 */ 180 */
@@ -207,7 +212,7 @@ class LinkFilter
207 foreach ($this->links as $id => $link) { 212 foreach ($this->links as $id => $link) {
208 // ignore non private links when 'privatonly' is on. 213 // ignore non private links when 'privatonly' is on.
209 if ($visibility !== 'all') { 214 if ($visibility !== 'all') {
210 if (! $link['private'] && $visibility === 'private') { 215 if (!$link['private'] && $visibility === 'private') {
211 continue; 216 continue;
212 } elseif ($link['private'] && $visibility === 'public') { 217 } elseif ($link['private'] && $visibility === 'public') {
213 continue; 218 continue;
@@ -250,7 +255,9 @@ class LinkFilter
250 255
251 /** 256 /**
252 * generate a regex fragment out of a tag 257 * generate a regex fragment out of a tag
258 *
253 * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard 259 * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
260 *
254 * @return string generated regex fragment 261 * @return string generated regex fragment
255 */ 262 */
256 private static function tag2regex($tag) 263 private static function tag2regex($tag)
@@ -334,7 +341,7 @@ class LinkFilter
334 // check level of visibility 341 // check level of visibility
335 // ignore non private links when 'privateonly' is on. 342 // ignore non private links when 'privateonly' is on.
336 if ($visibility !== 'all') { 343 if ($visibility !== 'all') {
337 if (! $link['private'] && $visibility === 'private') { 344 if (!$link['private'] && $visibility === 'private') {
338 continue; 345 continue;
339 } elseif ($link['private'] && $visibility === 'public') { 346 } elseif ($link['private'] && $visibility === 'public') {
340 continue; 347 continue;
@@ -377,7 +384,7 @@ class LinkFilter
377 $filtered = []; 384 $filtered = [];
378 foreach ($this->links as $key => $link) { 385 foreach ($this->links as $key => $link) {
379 if ($visibility !== 'all') { 386 if ($visibility !== 'all') {
380 if (! $link['private'] && $visibility === 'private') { 387 if (!$link['private'] && $visibility === 'private') {
381 continue; 388 continue;
382 } elseif ($link['private'] && $visibility === 'public') { 389 } elseif ($link['private'] && $visibility === 'public') {
383 continue; 390 continue;
@@ -406,7 +413,7 @@ class LinkFilter
406 */ 413 */
407 public function filterDay($day) 414 public function filterDay($day)
408 { 415 {
409 if (! checkDateFormat('Ymd', $day)) { 416 if (!checkDateFormat('Ymd', $day)) {
410 throw new Exception('Invalid date format'); 417 throw new Exception('Invalid date format');
411 } 418 }
412 419
@@ -440,14 +447,3 @@ class LinkFilter
440 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); 447 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
441 } 448 }
442} 449}
443
444class LinkNotFoundException extends Exception
445{
446 /**
447 * LinkNotFoundException constructor.
448 */
449 public function __construct()
450 {
451 $this->message = t('The link you are trying to reach does not exist or has been deleted.');
452 }
453}
diff --git a/application/LinkUtils.php b/application/bookmark/LinkUtils.php
index d56e019f..77eb2d95 100644
--- a/application/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -1,17 +1,31 @@
1<?php 1<?php
2 2
3use Shaarli\Bookmark\LinkDB;
4
3/** 5/**
4 * Get cURL callback function for CURLOPT_WRITEFUNCTION 6 * Get cURL callback function for CURLOPT_WRITEFUNCTION
5 * 7 *
6 * @param string $charset to extract from the downloaded page (reference) 8 * @param string $charset to extract from the downloaded page (reference)
7 * @param string $title to extract from the downloaded page (reference) 9 * @param string $title to extract from the downloaded page (reference)
8 * @param string $curlGetInfo Optionnaly overrides curl_getinfo function 10 * @param string $description to extract from the downloaded page (reference)
11 * @param string $keywords to extract from the downloaded page (reference)
12 * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
13 * @param string $curlGetInfo Optionally overrides curl_getinfo function
9 * 14 *
10 * @return Closure 15 * @return Closure
11 */ 16 */
12function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo') 17function get_curl_download_callback(
13{ 18 &$charset,
19 &$title,
20 &$description,
21 &$keywords,
22 $retrieveDescription,
23 $curlGetInfo = 'curl_getinfo'
24) {
14 $isRedirected = false; 25 $isRedirected = false;
26 $currentChunk = 0;
27 $foundChunk = null;
28
15 /** 29 /**
16 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download). 30 * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
17 * 31 *
@@ -23,7 +37,18 @@ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_get
23 * 37 *
24 * @return int|bool length of $data or false if we need to stop the download 38 * @return int|bool length of $data or false if we need to stop the download
25 */ 39 */
26 return function (&$ch, $data) use ($curlGetInfo, &$charset, &$title, &$isRedirected) { 40 return function (&$ch, $data) use (
41 $retrieveDescription,
42 $curlGetInfo,
43 &$charset,
44 &$title,
45 &$description,
46 &$keywords,
47 &$isRedirected,
48 &$currentChunk,
49 &$foundChunk
50 ) {
51 $currentChunk++;
27 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE); 52 $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
28 if (!empty($responseCode) && in_array($responseCode, [301, 302])) { 53 if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
29 $isRedirected = true; 54 $isRedirected = true;
@@ -48,9 +73,34 @@ function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_get
48 } 73 }
49 if (empty($title)) { 74 if (empty($title)) {
50 $title = html_extract_title($data); 75 $title = html_extract_title($data);
76 $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
77 }
78 if ($retrieveDescription && empty($description)) {
79 $description = html_extract_tag('description', $data);
80 $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
51 } 81 }
82 if ($retrieveDescription && empty($keywords)) {
83 $keywords = html_extract_tag('keywords', $data);
84 if (! empty($keywords)) {
85 $foundChunk = $currentChunk;
86 // Keywords use the format tag1, tag2 multiple words, tag
87 // So we format them to match Shaarli's separator and glue multiple words with '-'
88 $keywords = implode(' ', array_map(function($keyword) {
89 return implode('-', preg_split('/\s+/', trim($keyword)));
90 }, explode(',', $keywords)));
91 }
92 }
93
52 // We got everything we want, stop the download. 94 // We got everything we want, stop the download.
53 if (!empty($responseCode) && !empty($contentType) && !empty($charset) && !empty($title)) { 95 // If we already found either the title, description or keywords,
96 // it's highly unlikely that we'll found the other metas further than
97 // in the same chunk of data or the next one. So we also stop the download after that.
98 if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
99 && (! $retrieveDescription
100 || $foundChunk < $currentChunk
101 || (!empty($title) && !empty($description) && !empty($keywords))
102 )
103 ) {
54 return false; 104 return false;
55 } 105 }
56 106
@@ -109,6 +159,35 @@ function html_extract_charset($html)
109} 159}
110 160
111/** 161/**
162 * Extract meta tag from HTML content in either:
163 * - OpenGraph: <meta property="og:[tag]" ...>
164 * - Meta tag: <meta name="[tag]" ...>
165 *
166 * @param string $tag Name of the tag to retrieve.
167 * @param string $html HTML content where to look for charset.
168 *
169 * @return bool|string Charset string if found, false otherwise.
170 */
171function html_extract_tag($tag, $html)
172{
173 $propertiesKey = ['property', 'name', 'itemprop'];
174 $properties = implode('|', $propertiesKey);
175 // Try to retrieve OpenGraph image.
176 $ogRegex = '#<meta[^>]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#';
177 // If the attributes are not in the order property => content (e.g. Github)
178 // New regex to keep this readable... more or less.
179 $ogRegexReverse = '#<meta[^>]+content=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#';
180
181 if (preg_match($ogRegex, $html, $matches) > 0
182 || preg_match($ogRegexReverse, $html, $matches) > 0
183 ) {
184 return $matches[1];
185 }
186
187 return false;
188}
189
190/**
112 * Count private links in given linklist. 191 * Count private links in given linklist.
113 * 192 *
114 * @param array|Countable $links Linklist. 193 * @param array|Countable $links Linklist.
@@ -131,29 +210,15 @@ function count_private($links)
131 * In a string, converts URLs to clickable links. 210 * In a string, converts URLs to clickable links.
132 * 211 *
133 * @param string $text input string. 212 * @param string $text input string.
134 * @param string $redirector if a redirector is set, use it to gerenate links.
135 * @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
136 * 213 *
137 * @return string returns $text with all links converted to HTML links. 214 * @return string returns $text with all links converted to HTML links.
138 * 215 *
139 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 216 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
140 */ 217 */
141function text2clickable($text, $redirector = '', $urlEncode = true) 218function text2clickable($text)
142{ 219{
143 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si'; 220 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
144 221 return preg_replace($regex, '<a href="$1">$1</a>', $text);
145 if (empty($redirector)) {
146 return preg_replace($regex, '<a href="$1">$1</a>', $text);
147 }
148 // Redirector is set, urlencode the final URL.
149 return preg_replace_callback(
150 $regex,
151 function ($matches) use ($redirector, $urlEncode) {
152 $url = $urlEncode ? urlencode($matches[1]) : $matches[1];
153 return '<a href="' . $redirector . $url .'">'. $matches[1] .'</a>';
154 },
155 $text
156 );
157} 222}
158 223
159/** 224/**
@@ -195,15 +260,13 @@ function space2nbsp($text)
195 * Format Shaarli's description 260 * Format Shaarli's description
196 * 261 *
197 * @param string $description shaare's description. 262 * @param string $description shaare's description.
198 * @param string $redirector if a redirector is set, use it to gerenate links.
199 * @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
200 * @param string $indexUrl URL to Shaarli's index. 263 * @param string $indexUrl URL to Shaarli's index.
201 264
202 * @return string formatted description. 265 * @return string formatted description.
203 */ 266 */
204function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '') 267function format_description($description, $indexUrl = '')
205{ 268{
206 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl))); 269 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl)));
207} 270}
208 271
209/** 272/**
@@ -218,3 +281,16 @@ function link_small_hash($date, $id)
218{ 281{
219 return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id); 282 return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id);
220} 283}
284
285/**
286 * Returns whether or not the link is an internal note.
287 * Its URL starts by `?` because it's actually a permalink.
288 *
289 * @param string $linkUrl
290 *
291 * @return bool true if internal note, false otherwise.
292 */
293function is_note($linkUrl)
294{
295 return isset($linkUrl[0]) && $linkUrl[0] === '?';
296}
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 @@
1<?php
2namespace Shaarli\Bookmark\Exception;
3
4use Exception;
5
6class LinkNotFoundException extends Exception
7{
8 /**
9 * LinkNotFoundException constructor.
10 */
11 public function __construct()
12 {
13 $this->message = t('The link you are trying to reach does not exist or has been deleted.');
14 }
15}
diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php
index 8c8d5610..4509357c 100644
--- a/application/config/ConfigJson.php
+++ b/application/config/ConfigJson.php
@@ -47,7 +47,7 @@ class ConfigJson implements ConfigIO
47 $print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; 47 $print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
48 $data = self::getPhpHeaders() . json_encode($conf, $print) . self::getPhpSuffix(); 48 $data = self::getPhpHeaders() . json_encode($conf, $print) . self::getPhpSuffix();
49 if (!file_put_contents($filepath, $data)) { 49 if (!file_put_contents($filepath, $data)) {
50 throw new \IOException( 50 throw new \Shaarli\Exceptions\IOException(
51 $filepath, 51 $filepath,
52 t('Shaarli could not create the config file. '. 52 t('Shaarli could not create the config file. '.
53 'Please make sure Shaarli has the right to write in the folder is it installed in.') 53 'Please make sure Shaarli has the right to write in the folder is it installed in.')
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index 32aaea48..c95e6800 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -207,7 +207,7 @@ class ConfigManager
207 * 207 *
208 * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf. 208 * @throws MissingFieldConfigException: a mandatory field has not been provided in $conf.
209 * @throws UnauthorizedConfigException: user is not authorize to change configuration. 209 * @throws UnauthorizedConfigException: user is not authorize to change configuration.
210 * @throws \IOException: an error occurred while writing the new config file. 210 * @throws \Shaarli\Exceptions\IOException: an error occurred while writing the new config file.
211 */ 211 */
212 public function write($isLoggedIn) 212 public function write($isLoggedIn)
213 { 213 {
@@ -221,7 +221,6 @@ class ConfigManager
221 'general.title', 221 'general.title',
222 'general.header_link', 222 'general.header_link',
223 'privacy.default_private_links', 223 'privacy.default_private_links',
224 'redirector.url',
225 ); 224 );
226 225
227 // Only logged in user can alter config. 226 // Only logged in user can alter config.
@@ -366,6 +365,7 @@ class ConfigManager
366 $this->setEmpty('general.links_per_page', 20); 365 $this->setEmpty('general.links_per_page', 20);
367 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); 366 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
368 $this->setEmpty('general.default_note_title', 'Note: '); 367 $this->setEmpty('general.default_note_title', 'Note: ');
368 $this->setEmpty('general.retrieve_description', false);
369 369
370 $this->setEmpty('updates.check_updates', false); 370 $this->setEmpty('updates.check_updates', false);
371 $this->setEmpty('updates.check_updates_branch', 'stable'); 371 $this->setEmpty('updates.check_updates_branch', 'stable');
@@ -381,9 +381,6 @@ class ConfigManager
381 // default state of the 'remember me' checkbox of the login form 381 // default state of the 'remember me' checkbox of the login form
382 $this->setEmpty('privacy.remember_user_default', true); 382 $this->setEmpty('privacy.remember_user_default', true);
383 383
384 $this->setEmpty('redirector.url', '');
385 $this->setEmpty('redirector.encode_url', true);
386
387 $this->setEmpty('thumbnails.width', '125'); 384 $this->setEmpty('thumbnails.width', '125');
388 $this->setEmpty('thumbnails.height', '90'); 385 $this->setEmpty('thumbnails.height', '90');
389 386
diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php
index 9625fe1a..cad34594 100644
--- a/application/config/ConfigPhp.php
+++ b/application/config/ConfigPhp.php
@@ -27,7 +27,7 @@ class ConfigPhp implements ConfigIO
27 /** 27 /**
28 * Map legacy config keys with the new ones. 28 * Map legacy config keys with the new ones.
29 * If ConfigPhp is used, getting <newkey> will actually look for <legacykey>. 29 * If ConfigPhp is used, getting <newkey> will actually look for <legacykey>.
30 * The Updater will use this array to transform keys when switching to JSON. 30 * The updater will use this array to transform keys when switching to JSON.
31 * 31 *
32 * @var array current key => legacy key. 32 * @var array current key => legacy key.
33 */ 33 */
@@ -124,7 +124,7 @@ class ConfigPhp implements ConfigIO
124 if (!file_put_contents($filepath, $configStr) 124 if (!file_put_contents($filepath, $configStr)
125 || strcmp(file_get_contents($filepath), $configStr) != 0 125 || strcmp(file_get_contents($filepath), $configStr) != 0
126 ) { 126 ) {
127 throw new \IOException( 127 throw new \Shaarli\Exceptions\IOException(
128 $filepath, 128 $filepath,
129 t('Shaarli could not create the config file. '. 129 t('Shaarli could not create the config file. '.
130 'Please make sure Shaarli has the right to write in the folder is it installed in.') 130 'Please make sure Shaarli has the right to write in the folder is it installed in.')
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php
index 18e46b77..2aa25e5c 100644
--- a/application/exceptions/IOException.php
+++ b/application/exceptions/IOException.php
@@ -1,4 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Exceptions;
3
4use Exception;
2 5
3/** 6/**
4 * Exception class thrown when a filesystem access failure happens 7 * Exception class thrown when a filesystem access failure happens
@@ -17,6 +20,6 @@ class IOException extends Exception
17 { 20 {
18 $this->path = $path; 21 $this->path = $path;
19 $this->message = empty($message) ? t('Error accessing') : $message; 22 $this->message = empty($message) ? t('Error accessing') : $message;
20 $this->message .= ' "' . $this->path .'"'; 23 $this->message .= ' "' . $this->path . '"';
21 } 24 }
22} 25}
diff --git a/application/Cache.php b/application/feed/Cache.php
index e5d43e61..e5d43e61 100644
--- a/application/Cache.php
+++ b/application/feed/Cache.php
diff --git a/application/CachedPage.php b/application/feed/CachedPage.php
index e11cc52d..d809bdd9 100644
--- a/application/CachedPage.php
+++ b/application/feed/CachedPage.php
@@ -1,4 +1,7 @@
1<?php 1<?php
2
3namespace Shaarli\Feed;
4
2/** 5/**
3 * Simple cache system, mainly for the RSS/ATOM feeds 6 * Simple cache system, mainly for the RSS/ATOM feeds
4 */ 7 */
@@ -24,7 +27,7 @@ class CachedPage
24 { 27 {
25 // TODO: check write access to the cache directory 28 // TODO: check write access to the cache directory
26 $this->cacheDir = $cacheDir; 29 $this->cacheDir = $cacheDir;
27 $this->filename = $this->cacheDir.'/'.sha1($url).'.cache'; 30 $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
28 $this->shouldBeCached = $shouldBeCached; 31 $this->shouldBeCached = $shouldBeCached;
29 } 32 }
30 33
diff --git a/application/FeedBuilder.php b/application/feed/FeedBuilder.php
index 73fafcbe..7c859474 100644
--- a/application/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -1,4 +1,7 @@
1<?php 1<?php
2namespace Shaarli\Feed;
3
4use DateTime;
2 5
3/** 6/**
4 * FeedBuilder class. 7 * FeedBuilder class.
@@ -28,7 +31,7 @@ class FeedBuilder
28 public static $DEFAULT_NB_LINKS = 50; 31 public static $DEFAULT_NB_LINKS = 50;
29 32
30 /** 33 /**
31 * @var LinkDB instance. 34 * @var \Shaarli\Bookmark\LinkDB instance.
32 */ 35 */
33 protected $linkDB; 36 protected $linkDB;
34 37
@@ -38,12 +41,12 @@ class FeedBuilder
38 protected $feedType; 41 protected $feedType;
39 42
40 /** 43 /**
41 * @var array $_SERVER. 44 * @var array $_SERVER
42 */ 45 */
43 protected $serverInfo; 46 protected $serverInfo;
44 47
45 /** 48 /**
46 * @var array $_GET. 49 * @var array $_GET
47 */ 50 */
48 protected $userInput; 51 protected $userInput;
49 52
@@ -75,11 +78,12 @@ class FeedBuilder
75 /** 78 /**
76 * Feed constructor. 79 * Feed constructor.
77 * 80 *
78 * @param LinkDB $linkDB LinkDB instance. 81 * @param \Shaarli\Bookmark\LinkDB $linkDB LinkDB instance.
79 * @param string $feedType Type of feed. 82 * @param string $feedType Type of feed.
80 * @param array $serverInfo $_SERVER. 83 * @param array $serverInfo $_SERVER.
81 * @param array $userInput $_GET. 84 * @param array $userInput $_GET.
82 * @param boolean $isLoggedIn True if the user is currently logged in, false otherwise. 85 * @param boolean $isLoggedIn True if the user is currently logged in,
86 * false otherwise.
83 */ 87 */
84 public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn) 88 public function __construct($linkDB, $feedType, $serverInfo, $userInput, $isLoggedIn)
85 { 89 {
@@ -124,7 +128,7 @@ class FeedBuilder
124 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn; 128 $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
125 // Remove leading slash from REQUEST_URI. 129 // Remove leading slash from REQUEST_URI.
126 $data['self_link'] = escape(server_url($this->serverInfo)) 130 $data['self_link'] = escape(server_url($this->serverInfo))
127 . escape($this->serverInfo['REQUEST_URI']); 131 . escape($this->serverInfo['REQUEST_URI']);
128 $data['index_url'] = $pageaddr; 132 $data['index_url'] = $pageaddr;
129 $data['usepermalinks'] = $this->usePermalinks === true; 133 $data['usepermalinks'] = $this->usePermalinks === true;
130 $data['links'] = $linkDisplayed; 134 $data['links'] = $linkDisplayed;
@@ -142,18 +146,18 @@ class FeedBuilder
142 */ 146 */
143 protected function buildItem($link, $pageaddr) 147 protected function buildItem($link, $pageaddr)
144 { 148 {
145 $link['guid'] = $pageaddr .'?'. $link['shorturl']; 149 $link['guid'] = $pageaddr . '?' . $link['shorturl'];
146 // Check for both signs of a note: starting with ? and 7 chars long. 150 // Prepend the root URL for notes
147 if ($link['url'][0] === '?' && strlen($link['url']) === 7) { 151 if (is_note($link['url'])) {
148 $link['url'] = $pageaddr . $link['url']; 152 $link['url'] = $pageaddr . $link['url'];
149 } 153 }
150 if ($this->usePermalinks === true) { 154 if ($this->usePermalinks === true) {
151 $permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>'; 155 $permalink = '<a href="' . $link['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>';
152 } else { 156 } else {
153 $permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>'; 157 $permalink = '<a href="' . $link['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>';
154 } 158 }
155 $link['description'] = format_description($link['description'], '', false, $pageaddr); 159 $link['description'] = format_description($link['description'], $pageaddr);
156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; 160 $link['description'] .= PHP_EOL . '<br>&#8212; ' . $permalink;
157 161
158 $pubDate = $link['created']; 162 $pubDate = $link['created'];
159 $link['pub_iso_date'] = $this->getIsoDate($pubDate); 163 $link['pub_iso_date'] = $this->getIsoDate($pubDate);
@@ -164,7 +168,6 @@ class FeedBuilder
164 $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM); 168 $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
165 } else { 169 } else {
166 $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM); 170 $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);
167 ;
168 } 171 }
169 172
170 // Save the more recent item. 173 // Save the more recent item.
@@ -223,11 +226,11 @@ class FeedBuilder
223 public function getTypeLanguage() 226 public function getTypeLanguage()
224 { 227 {
225 // Use the locale do define the language, if available. 228 // Use the locale do define the language, if available.
226 if (! empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) { 229 if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
227 $length = ($this->feedType == self::$FEED_RSS) ? 5 : 2; 230 $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2;
228 return str_replace('_', '-', substr($this->locale, 0, $length)); 231 return str_replace('_', '-', substr($this->locale, 0, $length));
229 } 232 }
230 return ($this->feedType == self::$FEED_RSS) ? 'en-en' : 'en'; 233 return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en';
231 } 234 }
232 235
233 /** 236 /**
@@ -287,7 +290,7 @@ class FeedBuilder
287 } 290 }
288 291
289 $intNb = intval($this->userInput['nb']); 292 $intNb = intval($this->userInput['nb']);
290 if (! is_int($intNb) || $intNb == 0) { 293 if (!is_int($intNb) || $intNb == 0) {
291 return self::$DEFAULT_NB_LINKS; 294 return self::$DEFAULT_NB_LINKS;
292 } 295 }
293 296
diff --git a/application/Base64Url.php b/application/http/Base64Url.php
index 54d0fcd5..33fa7c1f 100644
--- a/application/Base64Url.php
+++ b/application/http/Base64Url.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3namespace Shaarli; 3namespace Shaarli\Http;
4 4
5/** 5/**
6 * URL-safe Base64 operations 6 * URL-safe Base64 operations
diff --git a/application/HttpUtils.php b/application/http/HttpUtils.php
index 9c438160..2ea9195d 100644
--- a/application/HttpUtils.php
+++ b/application/http/HttpUtils.php
@@ -1,4 +1,7 @@
1<?php 1<?php
2
3use Shaarli\Http\Url;
4
2/** 5/**
3 * GET an HTTP URL to retrieve its content 6 * GET an HTTP URL to retrieve its content
4 * Uses the cURL library or a fallback method 7 * Uses the cURL library or a fallback method
@@ -38,7 +41,7 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
38 $cleanUrl = $urlObj->idnToAscii(); 41 $cleanUrl = $urlObj->idnToAscii();
39 42
40 if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) { 43 if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) {
41 return array(array(0 => 'Invalid HTTP Url'), false); 44 return array(array(0 => 'Invalid HTTP UrlUtils'), false);
42 } 45 }
43 46
44 $userAgent = 47 $userAgent =
diff --git a/application/Url.php b/application/http/Url.php
index 3b7f19c2..90444a2f 100644
--- a/application/Url.php
+++ b/application/http/Url.php
@@ -1,91 +1,6 @@
1<?php 1<?php
2/**
3 * Converts an array-represented URL to a string
4 *
5 * Source: http://php.net/manual/en/function.parse-url.php#106731
6 *
7 * @see http://php.net/manual/en/function.parse-url.php
8 *
9 * @param array $parsedUrl an array-represented URL
10 *
11 * @return string the string representation of the URL
12 */
13function unparse_url($parsedUrl)
14{
15 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'].'://' : '';
16 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
17 $port = isset($parsedUrl['port']) ? ':'.$parsedUrl['port'] : '';
18 $user = isset($parsedUrl['user']) ? $parsedUrl['user'] : '';
19 $pass = isset($parsedUrl['pass']) ? ':'.$parsedUrl['pass'] : '';
20 $pass = ($user || $pass) ? "$pass@" : '';
21 $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
22 $query = isset($parsedUrl['query']) ? '?'.$parsedUrl['query'] : '';
23 $fragment = isset($parsedUrl['fragment']) ? '#'.$parsedUrl['fragment'] : '';
24 2
25 return "$scheme$user$pass$host$port$path$query$fragment"; 3namespace Shaarli\Http;
26}
27
28/**
29 * Removes undesired query parameters and fragments
30 *
31 * @param string url Url to be cleaned
32 *
33 * @return string the string representation of this URL after cleanup
34 */
35function cleanup_url($url)
36{
37 $obj_url = new Url($url);
38 return $obj_url->cleanup();
39}
40
41/**
42 * Get URL scheme.
43 *
44 * @param string url Url for which the scheme is requested
45 *
46 * @return mixed the URL scheme or false if none is provided.
47 */
48function get_url_scheme($url)
49{
50 $obj_url = new Url($url);
51 return $obj_url->getScheme();
52}
53
54/**
55 * Adds a trailing slash at the end of URL if necessary.
56 *
57 * @param string $url URL to check/edit.
58 *
59 * @return string $url URL with a end trailing slash.
60 */
61function add_trailing_slash($url)
62{
63 return $url . (!endsWith($url, '/') ? '/' : '');
64}
65
66/**
67 * Replace not whitelisted protocols by 'http://' from given URL.
68 *
69 * @param string $url URL to clean
70 * @param array $protocols List of allowed protocols (aside from http(s)).
71 *
72 * @return string URL with allowed protocol
73 */
74function whitelist_protocols($url, $protocols)
75{
76 if (startsWith($url, '?') || startsWith($url, '/')) {
77 return $url;
78 }
79 $protocols = array_merge(['http', 'https'], $protocols);
80 $protocol = preg_match('#^(\w+):/?/?#', $url, $match);
81 // Protocol not allowed: we remove it and replace it with http
82 if ($protocol === 1 && ! in_array($match[1], $protocols)) {
83 $url = str_replace($match[0], 'http://', $url);
84 } elseif ($protocol !== 1) {
85 $url = 'http://' . $url;
86 }
87 return $url;
88}
89 4
90/** 5/**
91 * URL representation and cleanup utilities 6 * URL representation and cleanup utilities
@@ -182,7 +97,7 @@ class Url
182 } 97 }
183 return $input; 98 return $input;
184 } 99 }
185 100
186 /** 101 /**
187 * Returns a string representation of this URL 102 * Returns a string representation of this URL
188 */ 103 */
@@ -196,7 +111,7 @@ class Url
196 */ 111 */
197 protected function cleanupQuery() 112 protected function cleanupQuery()
198 { 113 {
199 if (! isset($this->parts['query'])) { 114 if (!isset($this->parts['query'])) {
200 return; 115 return;
201 } 116 }
202 117
@@ -224,7 +139,7 @@ class Url
224 */ 139 */
225 protected function cleanupFragment() 140 protected function cleanupFragment()
226 { 141 {
227 if (! isset($this->parts['fragment'])) { 142 if (!isset($this->parts['fragment'])) {
228 return; 143 return;
229 } 144 }
230 145
@@ -257,7 +172,7 @@ class Url
257 public function idnToAscii() 172 public function idnToAscii()
258 { 173 {
259 $out = $this->cleanup(); 174 $out = $this->cleanup();
260 if (! function_exists('idn_to_ascii') || ! isset($this->parts['host'])) { 175 if (!function_exists('idn_to_ascii') || !isset($this->parts['host'])) {
261 return $out; 176 return $out;
262 } 177 }
263 $asciiHost = idn_to_ascii($this->parts['host'], 0, INTL_IDNA_VARIANT_UTS46); 178 $asciiHost = idn_to_ascii($this->parts['host'], 0, INTL_IDNA_VARIANT_UTS46);
@@ -291,7 +206,7 @@ class Url
291 } 206 }
292 207
293 /** 208 /**
294 * Test if the Url is an HTTP one. 209 * Test if the UrlUtils is an HTTP one.
295 * 210 *
296 * @return true is HTTP, false otherwise. 211 * @return true is HTTP, false otherwise.
297 */ 212 */
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 @@
1<?php
2/**
3 * Converts an array-represented URL to a string
4 *
5 * Source: http://php.net/manual/en/function.parse-url.php#106731
6 *
7 * @see http://php.net/manual/en/function.parse-url.php
8 *
9 * @param array $parsedUrl an array-represented URL
10 *
11 * @return string the string representation of the URL
12 */
13function unparse_url($parsedUrl)
14{
15 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'].'://' : '';
16 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
17 $port = isset($parsedUrl['port']) ? ':'.$parsedUrl['port'] : '';
18 $user = isset($parsedUrl['user']) ? $parsedUrl['user'] : '';
19 $pass = isset($parsedUrl['pass']) ? ':'.$parsedUrl['pass'] : '';
20 $pass = ($user || $pass) ? "$pass@" : '';
21 $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
22 $query = isset($parsedUrl['query']) ? '?'.$parsedUrl['query'] : '';
23 $fragment = isset($parsedUrl['fragment']) ? '#'.$parsedUrl['fragment'] : '';
24
25 return "$scheme$user$pass$host$port$path$query$fragment";
26}
27
28/**
29 * Removes undesired query parameters and fragments
30 *
31 * @param string url UrlUtils to be cleaned
32 *
33 * @return string the string representation of this URL after cleanup
34 */
35function cleanup_url($url)
36{
37 $obj_url = new \Shaarli\Http\Url($url);
38 return $obj_url->cleanup();
39}
40
41/**
42 * Get URL scheme.
43 *
44 * @param string url UrlUtils for which the scheme is requested
45 *
46 * @return mixed the URL scheme or false if none is provided.
47 */
48function get_url_scheme($url)
49{
50 $obj_url = new \Shaarli\Http\Url($url);
51 return $obj_url->getScheme();
52}
53
54/**
55 * Adds a trailing slash at the end of URL if necessary.
56 *
57 * @param string $url URL to check/edit.
58 *
59 * @return string $url URL with a end trailing slash.
60 */
61function add_trailing_slash($url)
62{
63 return $url . (!endsWith($url, '/') ? '/' : '');
64}
65
66/**
67 * Replace not whitelisted protocols by 'http://' from given URL.
68 *
69 * @param string $url URL to clean
70 * @param array $protocols List of allowed protocols (aside from http(s)).
71 *
72 * @return string URL with allowed protocol
73 */
74function whitelist_protocols($url, $protocols)
75{
76 if (startsWith($url, '?') || startsWith($url, '/')) {
77 return $url;
78 }
79 $protocols = array_merge(['http', 'https'], $protocols);
80 $protocol = preg_match('#^(\w+):/?/?#', $url, $match);
81 // Protocol not allowed: we remove it and replace it with http
82 if ($protocol === 1 && ! in_array($match[1], $protocols)) {
83 $url = str_replace($match[0], 'http://', $url);
84 } elseif ($protocol !== 1) {
85 $url = 'http://' . $url;
86 }
87 return $url;
88}
diff --git a/application/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php
index 84dd2b20..28665941 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/netscape/NetscapeBookmarkUtils.php
@@ -1,9 +1,16 @@
1<?php 1<?php
2 2
3namespace Shaarli\Netscape;
4
5use DateTime;
6use DateTimeZone;
7use Exception;
8use Katzgrau\KLogger\Logger;
3use Psr\Log\LogLevel; 9use Psr\Log\LogLevel;
10use Shaarli\Bookmark\LinkDB;
4use Shaarli\Config\ConfigManager; 11use Shaarli\Config\ConfigManager;
12use Shaarli\History;
5use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; 13use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser;
6use Katzgrau\KLogger\Logger;
7 14
8/** 15/**
9 * Utilities to import and export bookmarks using the Netscape format 16 * Utilities to import and export bookmarks using the Netscape format
@@ -31,8 +38,8 @@ class NetscapeBookmarkUtils
31 public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $indexUrl) 38 public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $indexUrl)
32 { 39 {
33 // see tpl/export.html for possible values 40 // see tpl/export.html for possible values
34 if (! in_array($selection, array('all', 'public', 'private'))) { 41 if (!in_array($selection, array('all', 'public', 'private'))) {
35 throw new Exception(t('Invalid export selection:') .' "'.$selection.'"'); 42 throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
36 } 43 }
37 44
38 $bookmarkLinks = array(); 45 $bookmarkLinks = array();
@@ -47,7 +54,7 @@ class NetscapeBookmarkUtils
47 $link['timestamp'] = $date->getTimestamp(); 54 $link['timestamp'] = $date->getTimestamp();
48 $link['taglist'] = str_replace(' ', ',', $link['tags']); 55 $link['taglist'] = str_replace(' ', ',', $link['tags']);
49 56
50 if (startsWith($link['url'], '?') && $prependNoteUrl) { 57 if (is_note($link['url']) && $prependNoteUrl) {
51 $link['url'] = $indexUrl . $link['url']; 58 $link['url'] = $indexUrl . $link['url'];
52 } 59 }
53 60
@@ -84,7 +91,7 @@ class NetscapeBookmarkUtils
84 $status .= vsprintf( 91 $status .= vsprintf(
85 t( 92 t(
86 'was successfully processed in %d seconds: ' 93 'was successfully processed in %d seconds: '
87 .'%d links imported, %d links overwritten, %d links skipped.' 94 . '%d links imported, %d links overwritten, %d links skipped.'
88 ), 95 ),
89 [$duration, $importCount, $overwriteCount, $skipCount] 96 [$duration, $importCount, $overwriteCount, $skipCount]
90 ); 97 );
@@ -95,11 +102,11 @@ class NetscapeBookmarkUtils
95 /** 102 /**
96 * Imports Web bookmarks from an uploaded Netscape bookmark dump 103 * Imports Web bookmarks from an uploaded Netscape bookmark dump
97 * 104 *
98 * @param array $post Server $_POST parameters 105 * @param array $post Server $_POST parameters
99 * @param array $files Server $_FILES parameters 106 * @param array $files Server $_FILES parameters
100 * @param LinkDB $linkDb Loaded LinkDB instance 107 * @param LinkDB $linkDb Loaded LinkDB instance
101 * @param ConfigManager $conf instance 108 * @param ConfigManager $conf instance
102 * @param History $history History instance 109 * @param History $history History instance
103 * 110 *
104 * @return string Summary of the bookmark import status 111 * @return string Summary of the bookmark import status
105 */ 112 */
@@ -115,7 +122,7 @@ class NetscapeBookmarkUtils
115 } 122 }
116 123
117 // Overwrite existing links? 124 // Overwrite existing links?
118 $overwrite = ! empty($post['overwrite']); 125 $overwrite = !empty($post['overwrite']);
119 126
120 // Add tags to all imported links? 127 // Add tags to all imported links?
121 if (empty($post['default_tags'])) { 128 if (empty($post['default_tags'])) {
@@ -138,7 +145,7 @@ class NetscapeBookmarkUtils
138 ); 145 );
139 $logger = new Logger( 146 $logger = new Logger(
140 $conf->get('resource.data_dir'), 147 $conf->get('resource.data_dir'),
141 ! $conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG, 148 !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
142 [ 149 [
143 'prefix' => 'import.', 150 'prefix' => 'import.',
144 'extension' => 'log', 151 'extension' => 'log',
@@ -193,7 +200,7 @@ class NetscapeBookmarkUtils
193 } 200 }
194 201
195 // Add a new link - @ used for UNIX timestamps 202 // Add a new link - @ used for UNIX timestamps
196 $newLinkDate = new DateTime('@'.strval($bkm['time'])); 203 $newLinkDate = new DateTime('@' . strval($bkm['time']));
197 $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get())); 204 $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
198 $newLink['created'] = $newLinkDate; 205 $newLink['created'] = $newLinkDate;
199 $newLink['id'] = $linkDb->getNextId(); 206 $newLink['id'] = $linkDb->getNextId();
diff --git a/application/PluginManager.php b/application/plugin/PluginManager.php
index 1ed4db4b..f7b24a8e 100644
--- a/application/PluginManager.php
+++ b/application/plugin/PluginManager.php
@@ -1,4 +1,8 @@
1<?php 1<?php
2namespace Shaarli\Plugin;
3
4use Shaarli\Config\ConfigManager;
5use Shaarli\Plugin\Exception\PluginFileNotFoundException;
2 6
3/** 7/**
4 * Class PluginManager 8 * Class PluginManager
@@ -9,12 +13,14 @@ class PluginManager
9{ 13{
10 /** 14 /**
11 * List of authorized plugins from configuration file. 15 * List of authorized plugins from configuration file.
16 *
12 * @var array $authorizedPlugins 17 * @var array $authorizedPlugins
13 */ 18 */
14 private $authorizedPlugins; 19 private $authorizedPlugins;
15 20
16 /** 21 /**
17 * List of loaded plugins. 22 * List of loaded plugins.
23 *
18 * @var array $loadedPlugins 24 * @var array $loadedPlugins
19 */ 25 */
20 private $loadedPlugins = array(); 26 private $loadedPlugins = array();
@@ -31,12 +37,14 @@ class PluginManager
31 37
32 /** 38 /**
33 * Plugins subdirectory. 39 * Plugins subdirectory.
40 *
34 * @var string $PLUGINS_PATH 41 * @var string $PLUGINS_PATH
35 */ 42 */
36 public static $PLUGINS_PATH = 'plugins'; 43 public static $PLUGINS_PATH = 'plugins';
37 44
38 /** 45 /**
39 * Plugins meta files extension. 46 * Plugins meta files extension.
47 *
40 * @var string $META_EXT 48 * @var string $META_EXT
41 */ 49 */
42 public static $META_EXT = 'meta'; 50 public static $META_EXT = 'meta';
@@ -84,9 +92,9 @@ class PluginManager
84 /** 92 /**
85 * Execute all plugins registered hook. 93 * Execute all plugins registered hook.
86 * 94 *
87 * @param string $hook name of the hook to trigger. 95 * @param string $hook name of the hook to trigger.
88 * @param array $data list of data to manipulate passed by reference. 96 * @param array $data list of data to manipulate passed by reference.
89 * @param array $params additional parameters such as page target. 97 * @param array $params additional parameters such as page target.
90 * 98 *
91 * @return void 99 * @return void
92 */ 100 */
@@ -118,7 +126,7 @@ class PluginManager
118 * @param string $pluginName plugin's name. 126 * @param string $pluginName plugin's name.
119 * 127 *
120 * @return void 128 * @return void
121 * @throws PluginFileNotFoundException - plugin files not found. 129 * @throws \Shaarli\Plugin\Exception\PluginFileNotFoundException - plugin files not found.
122 */ 130 */
123 private function loadPlugin($dir, $pluginName) 131 private function loadPlugin($dir, $pluginName)
124 { 132 {
@@ -204,8 +212,8 @@ class PluginManager
204 212
205 $metaData[$plugin]['parameters'][$param]['value'] = ''; 213 $metaData[$plugin]['parameters'][$param]['value'] = '';
206 // Optional parameter description in parameter.PARAM_NAME= 214 // Optional parameter description in parameter.PARAM_NAME=
207 if (isset($metaData[$plugin]['parameter.'. $param])) { 215 if (isset($metaData[$plugin]['parameter.' . $param])) {
208 $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]); 216 $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.' . $param]);
209 } 217 }
210 } 218 }
211 } 219 }
@@ -223,22 +231,3 @@ class PluginManager
223 return $this->errors; 231 return $this->errors;
224 } 232 }
225} 233}
226
227/**
228 * Class PluginFileNotFoundException
229 *
230 * Raise when plugin files can't be found.
231 */
232class PluginFileNotFoundException extends Exception
233{
234 /**
235 * Construct exception with plugin name.
236 * Generate message.
237 *
238 * @param string $pluginName name of the plugin not found
239 */
240 public function __construct($pluginName)
241 {
242 $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
243 }
244}
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 @@
1<?php
2namespace Shaarli\Plugin\Exception;
3
4use Exception;
5
6/**
7 * Class PluginFileNotFoundException
8 *
9 * Raise when plugin files can't be found.
10 */
11class PluginFileNotFoundException extends Exception
12{
13 /**
14 * Construct exception with plugin name.
15 * Generate message.
16 *
17 * @param string $pluginName name of the plugin not found
18 */
19 public function __construct($pluginName)
20 {
21 $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
22 }
23}
diff --git a/application/PageBuilder.php b/application/render/PageBuilder.php
index 2ca95832..3f86fc26 100644
--- a/application/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -1,5 +1,11 @@
1<?php 1<?php
2 2
3namespace Shaarli\Render;
4
5use Exception;
6use RainTPL;
7use Shaarli\ApplicationUtils;
8use Shaarli\Bookmark\LinkDB;
3use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
4use Shaarli\Thumbnailer; 10use Shaarli\Thumbnailer;
5 11
@@ -37,7 +43,9 @@ class PageBuilder
37 */ 43 */
38 protected $token; 44 protected $token;
39 45
40 /** @var bool $isLoggedIn Whether the user is logged in **/ 46 /**
47 * @var bool $isLoggedIn Whether the user is logged in
48 */
41 protected $isLoggedIn = false; 49 protected $isLoggedIn = false;
42 50
43 /** 51 /**
@@ -101,7 +109,7 @@ class PageBuilder
101 ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt')) 109 ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
102 ); 110 );
103 $this->tpl->assign('index_url', index_url($_SERVER)); 111 $this->tpl->assign('index_url', index_url($_SERVER));
104 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : ''; 112 $visibility = !empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
105 $this->tpl->assign('visibility', $visibility); 113 $this->tpl->assign('visibility', $visibility);
106 $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly'])); 114 $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
107 $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli')); 115 $this->tpl->assign('pagetitle', $this->conf->get('general.title', 'Shaarli'));
@@ -115,6 +123,8 @@ class PageBuilder
115 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); 123 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
116 $this->tpl->assign('token', $this->token); 124 $this->tpl->assign('token', $this->token);
117 125
126 $this->tpl->assign('language', $this->conf->get('translation.language'));
127
118 if ($this->linkDB !== null) { 128 if ($this->linkDB !== null) {
119 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); 129 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
120 } 130 }
@@ -126,7 +136,7 @@ class PageBuilder
126 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width')); 136 $this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
127 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height')); 137 $this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
128 138
129 if (! empty($_SESSION['warnings'])) { 139 if (!empty($_SESSION['warnings'])) {
130 $this->tpl->assign('global_warnings', $_SESSION['warnings']); 140 $this->tpl->assign('global_warnings', $_SESSION['warnings']);
131 unset($_SESSION['warnings']); 141 unset($_SESSION['warnings']);
132 } 142 }
@@ -189,16 +199,16 @@ class PageBuilder
189 199
190 /** 200 /**
191 * Render a 404 page (uses the template : tpl/404.tpl) 201 * Render a 404 page (uses the template : tpl/404.tpl)
192 * usage : $PAGE->render404('The link was deleted') 202 * usage: $PAGE->render404('The link was deleted')
193 * 203 *
194 * @param string $message A messate to display what is not found 204 * @param string $message A message to display what is not found
195 */ 205 */
196 public function render404($message = '') 206 public function render404($message = '')
197 { 207 {
198 if (empty($message)) { 208 if (empty($message)) {
199 $message = t('The page you are trying to reach does not exist or has been deleted.'); 209 $message = t('The page you are trying to reach does not exist or has been deleted.');
200 } 210 }
201 header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found')); 211 header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found'));
202 $this->tpl->assign('error_message', $message); 212 $this->tpl->assign('error_message', $message);
203 $this->renderPage('404'); 213 $this->renderPage('404');
204 } 214 }
diff --git a/application/ThemeUtils.php b/application/render/ThemeUtils.php
index 16f2f6a2..86096c64 100644
--- a/application/ThemeUtils.php
+++ b/application/render/ThemeUtils.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2 2
3namespace Shaarli; 3namespace Shaarli\Render;
4 4
5/** 5/**
6 * Class ThemeUtils 6 * Class ThemeUtils
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 @@
1<?php
2
3
4namespace Shaarli\Security;
5
6use Shaarli\FileUtils;
7
8/**
9 * Class BanManager
10 *
11 * Failed login attempts will store the associated IP address.
12 * After N failed attempts, the IP will be prevented from log in for duration D.
13 * Both N and D can be set in the configuration file.
14 *
15 * @package Shaarli\Security
16 */
17class BanManager
18{
19 /** @var array List of allowed proxies IP */
20 protected $trustedProxies;
21
22 /** @var int Number of allowed failed attempt before the ban */
23 protected $nbAttempts;
24
25 /** @var int Ban duration in seconds */
26 protected $banDuration;
27
28 /** @var string Path to the file containing IP bans and failures */
29 protected $banFile;
30
31 /** @var string Path to the log file, used to log bans */
32 protected $logFile;
33
34 /** @var array List of IP with their associated number of failed attempts */
35 protected $failures = [];
36
37 /** @var array List of banned IP with their associated unban timestamp */
38 protected $bans = [];
39
40 /**
41 * BanManager constructor.
42 *
43 * @param array $trustedProxies List of allowed proxies IP
44 * @param int $nbAttempts Number of allowed failed attempt before the ban
45 * @param int $banDuration Ban duration in seconds
46 * @param string $banFile Path to the file containing IP bans and failures
47 * @param string $logFile Path to the log file, used to log bans
48 */
49 public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) {
50 $this->trustedProxies = $trustedProxies;
51 $this->nbAttempts = $nbAttempts;
52 $this->banDuration = $banDuration;
53 $this->banFile = $banFile;
54 $this->logFile = $logFile;
55 $this->readBanFile();
56 }
57
58 /**
59 * Handle a failed login and ban the IP after too many failed attempts
60 *
61 * @param array $server The $_SERVER array
62 */
63 public function handleFailedAttempt($server)
64 {
65 $ip = $this->getIp($server);
66 // the IP is behind a trusted forward proxy, but is not forwarded
67 // in the HTTP headers, so we do nothing
68 if (empty($ip)) {
69 return;
70 }
71
72 // increment the fail count for this IP
73 if (isset($this->failures[$ip])) {
74 $this->failures[$ip]++;
75 } else {
76 $this->failures[$ip] = 1;
77 }
78
79 if ($this->failures[$ip] >= $this->nbAttempts) {
80 $this->bans[$ip] = time() + $this->banDuration;
81 logm(
82 $this->logFile,
83 $server['REMOTE_ADDR'],
84 'IP address banned from login: '. $ip
85 );
86 }
87 $this->writeBanFile();
88 }
89
90 /**
91 * Remove failed attempts for the provided client.
92 *
93 * @param array $server $_SERVER
94 */
95 public function clearFailures($server)
96 {
97 $ip = $this->getIp($server);
98 // the IP is behind a trusted forward proxy, but is not forwarded
99 // in the HTTP headers, so we do nothing
100 if (empty($ip)) {
101 return;
102 }
103
104 if (isset($this->failures[$ip])) {
105 unset($this->failures[$ip]);
106 }
107 $this->writeBanFile();
108 }
109
110 /**
111 * Check whether the client IP is banned or not.
112 *
113 * @param array $server $_SERVER
114 *
115 * @return bool True if the IP is banned, false otherwise
116 */
117 public function isBanned($server)
118 {
119 $ip = $this->getIp($server);
120 // the IP is behind a trusted forward proxy, but is not forwarded
121 // in the HTTP headers, so we allow the authentication attempt.
122 if (empty($ip)) {
123 return false;
124 }
125
126 // the user is not banned
127 if (! isset($this->bans[$ip])) {
128 return false;
129 }
130
131 // the user is still banned
132 if ($this->bans[$ip] > time()) {
133 return true;
134 }
135
136 // the ban has expired, the user can attempt to log in again
137 if (isset($this->failures[$ip])) {
138 unset($this->failures[$ip]);
139 }
140 unset($this->bans[$ip]);
141 logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip);
142
143 $this->writeBanFile();
144 return false;
145 }
146
147 /**
148 * Retrieve the IP from $_SERVER.
149 * If the actual IP is behind an allowed reverse proxy,
150 * we try to extract the forwarded IP from HTTP headers.
151 *
152 * @param array $server $_SERVER
153 *
154 * @return string|bool The IP or false if none could be extracted
155 */
156 protected function getIp($server)
157 {
158 $ip = $server['REMOTE_ADDR'];
159 if (! in_array($ip, $this->trustedProxies)) {
160 return $ip;
161 }
162 return getIpAddressFromProxy($server, $this->trustedProxies);
163 }
164
165 /**
166 * Read a file containing banned IPs
167 */
168 protected function readBanFile()
169 {
170 $data = FileUtils::readFlatDB($this->banFile);
171 if (isset($data['failures']) && is_array($data['failures'])) {
172 $this->failures = $data['failures'];
173 }
174
175 if (isset($data['bans']) && is_array($data['bans'])) {
176 $this->bans = $data['bans'];
177 }
178 }
179
180 /**
181 * Write the banned IPs to a file
182 */
183 protected function writeBanFile()
184 {
185 return FileUtils::writeFlatDB(
186 $this->banFile,
187 [
188 'failures' => $this->failures,
189 'bans' => $this->bans,
190 ]
191 );
192 }
193
194 /**
195 * Get the Failures (for UT purpose).
196 *
197 * @return array
198 */
199 public function getFailures()
200 {
201 return $this->failures;
202 }
203
204 /**
205 * Get the Bans (for UT purpose).
206 *
207 * @return array
208 */
209 public function getBans()
210 {
211 return $this->bans;
212 }
213}
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
index 0f315483..0b0ce0b1 100644
--- a/application/security/LoginManager.php
+++ b/application/security/LoginManager.php
@@ -20,8 +20,8 @@ class LoginManager
20 /** @var SessionManager Session Manager instance **/ 20 /** @var SessionManager Session Manager instance **/
21 protected $sessionManager = null; 21 protected $sessionManager = null;
22 22
23 /** @var string Path to the file containing IP bans */ 23 /** @var BanManager Ban Manager instance **/
24 protected $banFile = ''; 24 protected $banManager;
25 25
26 /** @var bool Whether the user is logged in **/ 26 /** @var bool Whether the user is logged in **/
27 protected $isLoggedIn = false; 27 protected $isLoggedIn = false;
@@ -35,17 +35,21 @@ class LoginManager
35 /** 35 /**
36 * Constructor 36 * Constructor
37 * 37 *
38 * @param array $globals The $GLOBALS array (reference)
39 * @param ConfigManager $configManager Configuration Manager instance 38 * @param ConfigManager $configManager Configuration Manager instance
40 * @param SessionManager $sessionManager SessionManager instance 39 * @param SessionManager $sessionManager SessionManager instance
41 */ 40 */
42 public function __construct(& $globals, $configManager, $sessionManager) 41 public function __construct($configManager, $sessionManager)
43 { 42 {
44 $this->globals = &$globals;
45 $this->configManager = $configManager; 43 $this->configManager = $configManager;
46 $this->sessionManager = $sessionManager; 44 $this->sessionManager = $sessionManager;
47 $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); 45 $this->banManager = new BanManager(
48 $this->readBanFile(); 46 $this->configManager->get('security.trusted_proxies', []),
47 $this->configManager->get('security.ban_after'),
48 $this->configManager->get('security.ban_duration'),
49 $this->configManager->get('resource.ban_file', 'data/ipbans.php'),
50 $this->configManager->get('resource.log')
51 );
52
49 if ($this->configManager->get('security.open_shaarli') === true) { 53 if ($this->configManager->get('security.open_shaarli') === true) {
50 $this->openShaarli = true; 54 $this->openShaarli = true;
51 } 55 }
@@ -58,6 +62,9 @@ class LoginManager
58 */ 62 */
59 public function generateStaySignedInToken($clientIpAddress) 63 public function generateStaySignedInToken($clientIpAddress)
60 { 64 {
65 if ($this->configManager->get('security.session_protection_disabled') === true) {
66 $clientIpAddress = '';
67 }
61 $this->staySignedInToken = sha1( 68 $this->staySignedInToken = sha1(
62 $this->configManager->get('credentials.hash') 69 $this->configManager->get('credentials.hash')
63 . $clientIpAddress 70 . $clientIpAddress
@@ -155,65 +162,13 @@ class LoginManager
155 } 162 }
156 163
157 /** 164 /**
158 * Read a file containing banned IPs
159 */
160 protected function readBanFile()
161 {
162 if (! file_exists($this->banFile)) {
163 return;
164 }
165 include $this->banFile;
166 }
167
168 /**
169 * Write the banned IPs to a file
170 */
171 protected function writeBanFile()
172 {
173 if (! array_key_exists('IPBANS', $this->globals)) {
174 return;
175 }
176 file_put_contents(
177 $this->banFile,
178 "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>"
179 );
180 }
181
182 /**
183 * Handle a failed login and ban the IP after too many failed attempts 165 * Handle a failed login and ban the IP after too many failed attempts
184 * 166 *
185 * @param array $server The $_SERVER array 167 * @param array $server The $_SERVER array
186 */ 168 */
187 public function handleFailedLogin($server) 169 public function handleFailedLogin($server)
188 { 170 {
189 $ip = $server['REMOTE_ADDR']; 171 $this->banManager->handleFailedAttempt($server);
190 $trusted = $this->configManager->get('security.trusted_proxies', []);
191
192 if (in_array($ip, $trusted)) {
193 $ip = getIpAddressFromProxy($server, $trusted);
194 if (! $ip) {
195 // the IP is behind a trusted forward proxy, but is not forwarded
196 // in the HTTP headers, so we do nothing
197 return;
198 }
199 }
200
201 // increment the fail count for this IP
202 if (isset($this->globals['IPBANS']['FAILURES'][$ip])) {
203 $this->globals['IPBANS']['FAILURES'][$ip]++;
204 } else {
205 $this->globals['IPBANS']['FAILURES'][$ip] = 1;
206 }
207
208 if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) {
209 $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800);
210 logm(
211 $this->configManager->get('resource.log'),
212 $server['REMOTE_ADDR'],
213 'IP address banned from login'
214 );
215 }
216 $this->writeBanFile();
217 } 172 }
218 173
219 /** 174 /**
@@ -223,13 +178,7 @@ class LoginManager
223 */ 178 */
224 public function handleSuccessfulLogin($server) 179 public function handleSuccessfulLogin($server)
225 { 180 {
226 $ip = $server['REMOTE_ADDR']; 181 $this->banManager->clearFailures($server);
227 // FIXME unban when behind a trusted proxy?
228
229 unset($this->globals['IPBANS']['FAILURES'][$ip]);
230 unset($this->globals['IPBANS']['BANS'][$ip]);
231
232 $this->writeBanFile();
233 } 182 }
234 183
235 /** 184 /**
@@ -241,24 +190,6 @@ class LoginManager
241 */ 190 */
242 public function canLogin($server) 191 public function canLogin($server)
243 { 192 {
244 $ip = $server['REMOTE_ADDR']; 193 return ! $this->banManager->isBanned($server);
245
246 if (! isset($this->globals['IPBANS']['BANS'][$ip])) {
247 // the user is not banned
248 return true;
249 }
250
251 if ($this->globals['IPBANS']['BANS'][$ip] > time()) {
252 // the user is still banned
253 return false;
254 }
255
256 // the ban has expired, the user can attempt to log in again
257 logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.');
258 unset($this->globals['IPBANS']['FAILURES'][$ip]);
259 unset($this->globals['IPBANS']['BANS'][$ip]);
260
261 $this->writeBanFile();
262 return true;
263 } 194 }
264} 195}
diff --git a/application/Updater.php b/application/updater/Updater.php
index 86a21fc3..30e5247b 100644
--- a/application/Updater.php
+++ b/application/updater/Updater.php
@@ -1,11 +1,24 @@
1<?php 1<?php
2
3namespace Shaarli\Updater;
4
5use Exception;
6use RainTPL;
7use ReflectionClass;
8use ReflectionException;
9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\LinkDB;
12use Shaarli\Bookmark\LinkFilter;
2use Shaarli\Config\ConfigJson; 13use Shaarli\Config\ConfigJson;
3use Shaarli\Config\ConfigPhp;
4use Shaarli\Config\ConfigManager; 14use Shaarli\Config\ConfigManager;
15use Shaarli\Config\ConfigPhp;
16use Shaarli\Exceptions\IOException;
5use Shaarli\Thumbnailer; 17use Shaarli\Thumbnailer;
18use Shaarli\Updater\Exception\UpdaterException;
6 19
7/** 20/**
8 * Class Updater. 21 * Class updater.
9 * Used to update stuff when a new Shaarli's version is reached. 22 * Used to update stuff when a new Shaarli's version is reached.
10 * Update methods are ran only once, and the stored in a JSON file. 23 * Update methods are ran only once, and the stored in a JSON file.
11 */ 24 */
@@ -83,12 +96,12 @@ class Updater
83 } 96 }
84 97
85 if ($this->methods === null) { 98 if ($this->methods === null) {
86 throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.')); 99 throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
87 } 100 }
88 101
89 foreach ($this->methods as $method) { 102 foreach ($this->methods as $method) {
90 // Not an update method or already done, pass. 103 // Not an update method or already done, pass.
91 if (! startsWith($method->getName(), 'updateMethod') 104 if (!startsWith($method->getName(), 'updateMethod')
92 || in_array($method->getName(), $this->doneUpdates) 105 || in_array($method->getName(), $this->doneUpdates)
93 ) { 106 ) {
94 continue; 107 continue;
@@ -139,7 +152,7 @@ class Updater
139 } 152 }
140 } 153 }
141 $this->conf->write($this->isLoggedIn); 154 $this->conf->write($this->isLoggedIn);
142 unlink($this->conf->get('resource.data_dir').'/options.php'); 155 unlink($this->conf->get('resource.data_dir') . '/options.php');
143 } 156 }
144 157
145 return true; 158 return true;
@@ -174,10 +187,10 @@ class Updater
174 $subConfig = array('config', 'plugins'); 187 $subConfig = array('config', 'plugins');
175 foreach ($subConfig as $sub) { 188 foreach ($subConfig as $sub) {
176 foreach ($oldConfig[$sub] as $key => $value) { 189 foreach ($oldConfig[$sub] as $key => $value) {
177 if (isset($legacyMap[$sub .'.'. $key])) { 190 if (isset($legacyMap[$sub . '.' . $key])) {
178 $configKey = $legacyMap[$sub .'.'. $key]; 191 $configKey = $legacyMap[$sub . '.' . $key];
179 } else { 192 } else {
180 $configKey = $sub .'.'. $key; 193 $configKey = $sub . '.' . $key;
181 } 194 }
182 $this->conf->set($configKey, $value); 195 $this->conf->set($configKey, $value);
183 } 196 }
@@ -205,7 +218,6 @@ class Updater
205 try { 218 try {
206 $this->conf->set('general.title', escape($this->conf->get('general.title'))); 219 $this->conf->set('general.title', escape($this->conf->get('general.title')));
207 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link'))); 220 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
208 $this->conf->set('redirector.url', escape($this->conf->get('redirector.url')));
209 $this->conf->write($this->isLoggedIn); 221 $this->conf->write($this->isLoggedIn);
210 } catch (Exception $e) { 222 } catch (Exception $e) {
211 error_log($e->getMessage()); 223 error_log($e->getMessage());
@@ -233,7 +245,7 @@ class Updater
233 return true; 245 return true;
234 } 246 }
235 247
236 $save = $this->conf->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php'; 248 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
237 copy($this->conf->get('resource.datastore'), $save); 249 copy($this->conf->get('resource.datastore'), $save);
238 250
239 $links = array(); 251 $links = array();
@@ -309,7 +321,7 @@ class Updater
309 // We run the update only if this folder still contains the template files. 321 // We run the update only if this folder still contains the template files.
310 $tplDir = $this->conf->get('resource.raintpl_tpl'); 322 $tplDir = $this->conf->get('resource.raintpl_tpl');
311 $tplFile = $tplDir . '/linklist.html'; 323 $tplFile = $tplDir . '/linklist.html';
312 if (! file_exists($tplFile)) { 324 if (!file_exists($tplFile)) {
313 return true; 325 return true;
314 } 326 }
315 327
@@ -333,7 +345,7 @@ class Updater
333 */ 345 */
334 public function updateMethodMoveUserCss() 346 public function updateMethodMoveUserCss()
335 { 347 {
336 if (! is_file('inc/user.css')) { 348 if (!is_file('inc/user.css')) {
337 return true; 349 return true;
338 } 350 }
339 351
@@ -369,11 +381,11 @@ class Updater
369 */ 381 */
370 public function updateMethodPiwikUrl() 382 public function updateMethodPiwikUrl()
371 { 383 {
372 if (! $this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) { 384 if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
373 return true; 385 return true;
374 } 386 }
375 387
376 $this->conf->set('plugins.PIWIK_URL', 'http://'. $this->conf->get('plugins.PIWIK_URL')); 388 $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
377 $this->conf->write($this->isLoggedIn); 389 $this->conf->write($this->isLoggedIn);
378 390
379 return true; 391 return true;
@@ -483,11 +495,11 @@ class Updater
483 return true; 495 return true;
484 } 496 }
485 497
486 if (! $this->conf->exists('general.download_max_size')) { 498 if (!$this->conf->exists('general.download_max_size')) {
487 $this->conf->set('general.download_max_size', 1024*1024*4); 499 $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
488 } 500 }
489 501
490 if (! $this->conf->exists('general.download_timeout')) { 502 if (!$this->conf->exists('general.download_timeout')) {
491 $this->conf->set('general.download_timeout', 30); 503 $this->conf->set('general.download_timeout', 30);
492 } 504 }
493 505
@@ -539,97 +551,14 @@ class Updater
539 551
540 return true; 552 return true;
541 } 553 }
542}
543
544/**
545 * Class UpdaterException.
546 */
547class UpdaterException extends Exception
548{
549 /**
550 * @var string Method where the error occurred.
551 */
552 protected $method;
553 554
554 /** 555 /**
555 * @var Exception The parent exception. 556 * Remove redirector settings.
556 */ 557 */
557 protected $previous; 558 public function updateMethodRemoveRedirector()
558
559 /**
560 * Constructor.
561 *
562 * @param string $message Force the error message if set.
563 * @param string $method Method where the error occurred.
564 * @param Exception|bool $previous Parent exception.
565 */
566 public function __construct($message = '', $method = '', $previous = false)
567 { 559 {
568 $this->method = $method; 560 $this->conf->remove('redirector');
569 $this->previous = $previous; 561 $this->conf->write(true);
570 $this->message = $this->buildMessage($message); 562 return true;
571 }
572
573 /**
574 * Build the exception error message.
575 *
576 * @param string $message Optional given error message.
577 *
578 * @return string The built error message.
579 */
580 private function buildMessage($message)
581 {
582 $out = '';
583 if (! empty($message)) {
584 $out .= $message . PHP_EOL;
585 }
586
587 if (! empty($this->method)) {
588 $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
589 }
590
591 if (! empty($this->previous)) {
592 $out .= ' '. $this->previous->getMessage();
593 }
594
595 return $out;
596 }
597}
598
599/**
600 * Read the updates file, and return already done updates.
601 *
602 * @param string $updatesFilepath Updates file path.
603 *
604 * @return array Already done update methods.
605 */
606function read_updates_file($updatesFilepath)
607{
608 if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
609 $content = file_get_contents($updatesFilepath);
610 if (! empty($content)) {
611 return explode(';', $content);
612 }
613 }
614 return array();
615}
616
617/**
618 * Write updates file.
619 *
620 * @param string $updatesFilepath Updates file path.
621 * @param array $updates Updates array to write.
622 *
623 * @throws Exception Couldn't write version number.
624 */
625function write_updates_file($updatesFilepath, $updates)
626{
627 if (empty($updatesFilepath)) {
628 throw new Exception(t('Updates file path is not set, can\'t write updates.'));
629 }
630
631 $res = file_put_contents($updatesFilepath, implode(';', $updates));
632 if ($res === false) {
633 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
634 } 563 }
635} 564}
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 @@
1<?php
2
3/**
4 * Read the updates file, and return already done updates.
5 *
6 * @param string $updatesFilepath Updates file path.
7 *
8 * @return array Already done update methods.
9 */
10function read_updates_file($updatesFilepath)
11{
12 if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
13 $content = file_get_contents($updatesFilepath);
14 if (! empty($content)) {
15 return explode(';', $content);
16 }
17 }
18 return array();
19}
20
21/**
22 * Write updates file.
23 *
24 * @param string $updatesFilepath Updates file path.
25 * @param array $updates Updates array to write.
26 *
27 * @throws Exception Couldn't write version number.
28 */
29function write_updates_file($updatesFilepath, $updates)
30{
31 if (empty($updatesFilepath)) {
32 throw new Exception(t('Updates file path is not set, can\'t write updates.'));
33 }
34
35 $res = file_put_contents($updatesFilepath, implode(';', $updates));
36 if ($res === false) {
37 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
38 }
39}
diff --git a/application/updater/exception/UpdaterException.php b/application/updater/exception/UpdaterException.php
new file mode 100644
index 00000000..20aceccf
--- /dev/null
+++ b/application/updater/exception/UpdaterException.php
@@ -0,0 +1,60 @@
1<?php
2
3namespace Shaarli\Updater\Exception;
4
5use Exception;
6
7/**
8 * Class UpdaterException.
9 */
10class UpdaterException extends Exception
11{
12 /**
13 * @var string Method where the error occurred.
14 */
15 protected $method;
16
17 /**
18 * @var Exception The parent exception.
19 */
20 protected $previous;
21
22 /**
23 * Constructor.
24 *
25 * @param string $message Force the error message if set.
26 * @param string $method Method where the error occurred.
27 * @param Exception|bool $previous Parent exception.
28 */
29 public function __construct($message = '', $method = '', $previous = false)
30 {
31 $this->method = $method;
32 $this->previous = $previous;
33 $this->message = $this->buildMessage($message);
34 }
35
36 /**
37 * Build the exception error message.
38 *
39 * @param string $message Optional given error message.
40 *
41 * @return string The built error message.
42 */
43 private function buildMessage($message)
44 {
45 $out = '';
46 if (!empty($message)) {
47 $out .= $message . PHP_EOL;
48 }
49
50 if (!empty($this->method)) {
51 $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
52 }
53
54 if (!empty($this->previous)) {
55 $out .= ' ' . $this->previous->getMessage();
56 }
57
58 return $out;
59 }
60}