diff options
Diffstat (limited to 'application')
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 |
2 | namespace Shaarli; | ||
3 | |||
4 | use Exception; | ||
5 | use 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 | ||
3 | require_once 'exceptions/IOException.php'; | 3 | namespace Shaarli; |
4 | |||
5 | use 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 |
2 | namespace Shaarli; | ||
3 | |||
4 | use DateTime; | ||
5 | use 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 @@ | |||
3 | namespace Shaarli; | 3 | namespace Shaarli; |
4 | 4 | ||
5 | use Gettext\GettextTranslator; | 5 | use Gettext\GettextTranslator; |
6 | use Gettext\Merge; | ||
7 | use Gettext\Translations; | 6 | use Gettext\Translations; |
8 | use Gettext\Translator; | 7 | use Gettext\Translator; |
9 | use Gettext\TranslatorInterface; | 8 | use 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 |
2 | namespace 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 @@ | |||
3 | namespace Shaarli; | 3 | namespace Shaarli; |
4 | 4 | ||
5 | use Shaarli\Config\ConfigManager; | 5 | use Shaarli\Config\ConfigManager; |
6 | use WebThumbnailer\Application\ConfigManager as WTConfigManager; | ||
6 | use WebThumbnailer\Exception\WebThumbnailerException; | 7 | use WebThumbnailer\Exception\WebThumbnailerException; |
7 | use WebThumbnailer\WebThumbnailer; | 8 | use WebThumbnailer\WebThumbnailer; |
8 | use 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 |
2 | namespace Shaarli\Api; | 2 | namespace Shaarli\Api; |
3 | 3 | ||
4 | use Shaarli\Api\Exceptions\ApiException; | ||
5 | use Shaarli\Api\Exceptions\ApiAuthorizationException; | 4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; |
6 | 5 | use Shaarli\Api\Exceptions\ApiException; | |
7 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
8 | use Slim\Container; | 7 | use Slim\Container; |
9 | use Slim\Http\Request; | 8 | use 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 |
2 | namespace Shaarli\Api; | 2 | namespace Shaarli\Api; |
3 | 3 | ||
4 | use Shaarli\Base64Url; | ||
5 | use Shaarli\Api\Exceptions\ApiAuthorizationException; | 4 | use Shaarli\Api\Exceptions\ApiAuthorizationException; |
5 | use 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 | ||
3 | namespace Shaarli\Api\Controllers; | 3 | namespace Shaarli\Api\Controllers; |
4 | 4 | ||
5 | use Shaarli\Bookmark\LinkDB; | ||
5 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
6 | use \Slim\Container; | 7 | use 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 | */ |
17 | class History extends ApiController | 17 | class 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 | ||
5 | use Shaarli\Api\ApiUtils; | 5 | use Shaarli\Api\ApiUtils; |
6 | use Shaarli\Api\Exceptions\ApiBadParametersException; | 6 | use Shaarli\Api\Exceptions\ApiBadParametersException; |
7 | use Shaarli\Api\Exceptions\ApiLinkNotFoundException; | ||
8 | use Shaarli\Api\Exceptions\ApiTagNotFoundException; | 7 | use Shaarli\Api\Exceptions\ApiTagNotFoundException; |
9 | use Slim\Http\Request; | 8 | use Slim\Http\Request; |
10 | use Slim\Http\Response; | 9 | use Slim\Http\Response; |
diff --git a/application/api/exceptions/ApiLinkNotFoundException.php b/application/api/exceptions/ApiLinkNotFoundException.php index c727f4f0..7c2bb56e 100644 --- a/application/api/exceptions/ApiLinkNotFoundException.php +++ b/application/api/exceptions/ApiLinkNotFoundException.php | |||
@@ -2,8 +2,6 @@ | |||
2 | 2 | ||
3 | namespace Shaarli\Api\Exceptions; | 3 | namespace Shaarli\Api\Exceptions; |
4 | 4 | ||
5 | use 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 | ||
3 | namespace Shaarli\Api\Exceptions; | 3 | namespace Shaarli\Api\Exceptions; |
4 | 4 | ||
5 | use 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 | |||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | use ArrayAccess; | ||
6 | use Countable; | ||
7 | use DateTime; | ||
8 | use Iterator; | ||
9 | use Shaarli\Bookmark\Exception\LinkNotFoundException; | ||
10 | use Shaarli\Exceptions\IOException; | ||
11 | use 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 | ||
256 | To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. | 249 | To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. |
257 | 250 | ||
258 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' | 251 | You 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 | ||
3 | namespace Shaarli\Bookmark; | ||
4 | |||
5 | use Exception; | ||
6 | use 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 | |||
444 | class 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 | ||
3 | use 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 | */ |
12 | function get_curl_download_callback(&$charset, &$title, $curlGetInfo = 'curl_getinfo') | 17 | function 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 | */ | ||
171 | function 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 | */ |
141 | function text2clickable($text, $redirector = '', $urlEncode = true) | 218 | function 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 | */ |
204 | function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '') | 267 | function 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 | */ | ||
293 | function 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 | ||
2 | namespace Shaarli\Bookmark\Exception; | ||
3 | |||
4 | use Exception; | ||
5 | |||
6 | class 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 |
2 | namespace Shaarli\Exceptions; | ||
3 | |||
4 | use 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 | |||
3 | namespace 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 |
2 | namespace Shaarli\Feed; | ||
3 | |||
4 | use 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>— '. $permalink; | 160 | $link['description'] .= PHP_EOL . '<br>— ' . $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 | ||
3 | namespace Shaarli; | 3 | namespace 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 | |||
3 | use 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 | */ | ||
13 | function 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"; | 3 | namespace 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 | */ | ||
35 | function 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 | */ | ||
48 | function 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 | */ | ||
61 | function 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 | */ | ||
74 | function 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 | */ | ||
13 | function 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 | */ | ||
35 | function 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 | */ | ||
48 | function 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 | */ | ||
61 | function 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 | */ | ||
74 | function 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 | ||
3 | namespace Shaarli\Netscape; | ||
4 | |||
5 | use DateTime; | ||
6 | use DateTimeZone; | ||
7 | use Exception; | ||
8 | use Katzgrau\KLogger\Logger; | ||
3 | use Psr\Log\LogLevel; | 9 | use Psr\Log\LogLevel; |
10 | use Shaarli\Bookmark\LinkDB; | ||
4 | use Shaarli\Config\ConfigManager; | 11 | use Shaarli\Config\ConfigManager; |
12 | use Shaarli\History; | ||
5 | use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; | 13 | use Shaarli\NetscapeBookmarkParser\NetscapeBookmarkParser; |
6 | use 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 |
2 | namespace Shaarli\Plugin; | ||
3 | |||
4 | use Shaarli\Config\ConfigManager; | ||
5 | use 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 | */ | ||
232 | class 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 | ||
2 | namespace Shaarli\Plugin\Exception; | ||
3 | |||
4 | use Exception; | ||
5 | |||
6 | /** | ||
7 | * Class PluginFileNotFoundException | ||
8 | * | ||
9 | * Raise when plugin files can't be found. | ||
10 | */ | ||
11 | class 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 | ||
3 | namespace Shaarli\Render; | ||
4 | |||
5 | use Exception; | ||
6 | use RainTPL; | ||
7 | use Shaarli\ApplicationUtils; | ||
8 | use Shaarli\Bookmark\LinkDB; | ||
3 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
4 | use Shaarli\Thumbnailer; | 10 | use 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 | ||
3 | namespace Shaarli; | 3 | namespace 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 | |||
4 | namespace Shaarli\Security; | ||
5 | |||
6 | use 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 | */ | ||
17 | class 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 | |||
3 | namespace Shaarli\Updater; | ||
4 | |||
5 | use Exception; | ||
6 | use RainTPL; | ||
7 | use ReflectionClass; | ||
8 | use ReflectionException; | ||
9 | use ReflectionMethod; | ||
10 | use Shaarli\ApplicationUtils; | ||
11 | use Shaarli\Bookmark\LinkDB; | ||
12 | use Shaarli\Bookmark\LinkFilter; | ||
2 | use Shaarli\Config\ConfigJson; | 13 | use Shaarli\Config\ConfigJson; |
3 | use Shaarli\Config\ConfigPhp; | ||
4 | use Shaarli\Config\ConfigManager; | 14 | use Shaarli\Config\ConfigManager; |
15 | use Shaarli\Config\ConfigPhp; | ||
16 | use Shaarli\Exceptions\IOException; | ||
5 | use Shaarli\Thumbnailer; | 17 | use Shaarli\Thumbnailer; |
18 | use 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 | */ | ||
547 | class 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 | */ | ||
606 | function 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 | */ | ||
625 | function 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 | */ | ||
10 | function 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 | */ | ||
29 | function 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 | |||
3 | namespace Shaarli\Updater\Exception; | ||
4 | |||
5 | use Exception; | ||
6 | |||
7 | /** | ||
8 | * Class UpdaterException. | ||
9 | */ | ||
10 | class 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 | } | ||