diff options
-rwxr-xr-x | application/Config.php | 129 | ||||
-rw-r--r-- | application/LinkDB.php | 124 | ||||
-rw-r--r-- | index.php | 205 | ||||
-rwxr-xr-x | tests/ConfigTest.php | 177 | ||||
-rw-r--r-- | tests/LinkDBTest.php | 4 | ||||
-rw-r--r-- | tests/utils/ReferenceLinkDB.php | 20 | ||||
-rw-r--r-- | tpl/dailyrss.html | 24 | ||||
-rw-r--r-- | tpl/editlink.html | 24 |
8 files changed, 545 insertions, 162 deletions
diff --git a/application/Config.php b/application/Config.php new file mode 100755 index 00000000..0b01b524 --- /dev/null +++ b/application/Config.php | |||
@@ -0,0 +1,129 @@ | |||
1 | <?php | ||
2 | /** | ||
3 | * Functions related to configuration management. | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * Re-write configuration file according to given array. | ||
8 | * Requires mandatory fields listed in $MANDATORY_FIELDS. | ||
9 | * | ||
10 | * @param array $config contains all configuration fields. | ||
11 | * @param bool $isLoggedIn true if user is logged in. | ||
12 | * | ||
13 | * @return void | ||
14 | * | ||
15 | * @throws MissingFieldConfigException: a mandatory field has not been provided in $config. | ||
16 | * @throws UnauthorizedConfigException: user is not authorize to change configuration. | ||
17 | * @throws Exception: an error occured while writing the new config file. | ||
18 | */ | ||
19 | function writeConfig($config, $isLoggedIn) | ||
20 | { | ||
21 | // These fields are required in configuration. | ||
22 | $MANDATORY_FIELDS = [ | ||
23 | 'login', 'hash', 'salt', 'timezone', 'title', 'titleLink', | ||
24 | 'redirector', 'disablesessionprotection', 'privateLinkByDefault' | ||
25 | ]; | ||
26 | |||
27 | if (!isset($config['config']['CONFIG_FILE'])) { | ||
28 | throw new MissingFieldConfigException('CONFIG_FILE'); | ||
29 | } | ||
30 | |||
31 | // Only logged in user can alter config. | ||
32 | if (is_file($config['config']['CONFIG_FILE']) && !$isLoggedIn) { | ||
33 | throw new UnauthorizedConfigException(); | ||
34 | } | ||
35 | |||
36 | // Check that all mandatory fields are provided in $config. | ||
37 | foreach ($MANDATORY_FIELDS as $field) { | ||
38 | if (!isset($config[$field])) { | ||
39 | throw new MissingFieldConfigException($field); | ||
40 | } | ||
41 | } | ||
42 | |||
43 | $configStr = '<?php '. PHP_EOL; | ||
44 | $configStr .= '$GLOBALS[\'login\'] = '.var_export($config['login'], true).';'. PHP_EOL; | ||
45 | $configStr .= '$GLOBALS[\'hash\'] = '.var_export($config['hash'], true).';'. PHP_EOL; | ||
46 | $configStr .= '$GLOBALS[\'salt\'] = '.var_export($config['salt'], true).'; '. PHP_EOL; | ||
47 | $configStr .= '$GLOBALS[\'timezone\'] = '.var_export($config['timezone'], true).';'. PHP_EOL; | ||
48 | $configStr .= 'date_default_timezone_set('.var_export($config['timezone'], true).');'. PHP_EOL; | ||
49 | $configStr .= '$GLOBALS[\'title\'] = '.var_export($config['title'], true).';'. PHP_EOL; | ||
50 | $configStr .= '$GLOBALS[\'titleLink\'] = '.var_export($config['titleLink'], true).'; '. PHP_EOL; | ||
51 | $configStr .= '$GLOBALS[\'redirector\'] = '.var_export($config['redirector'], true).'; '. PHP_EOL; | ||
52 | $configStr .= '$GLOBALS[\'disablesessionprotection\'] = '.var_export($config['disablesessionprotection'], true).'; '. PHP_EOL; | ||
53 | $configStr .= '$GLOBALS[\'privateLinkByDefault\'] = '.var_export($config['privateLinkByDefault'], true).'; '. PHP_EOL; | ||
54 | |||
55 | // Store all $config['config'] | ||
56 | foreach ($config['config'] as $key => $value) { | ||
57 | $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($config['config'][$key], true).';'. PHP_EOL; | ||
58 | } | ||
59 | $configStr .= '?>'; | ||
60 | |||
61 | if (!file_put_contents($config['config']['CONFIG_FILE'], $configStr) | ||
62 | || strcmp(file_get_contents($config['config']['CONFIG_FILE']), $configStr) != 0 | ||
63 | ) { | ||
64 | throw new Exception( | ||
65 | 'Shaarli could not create the config file. | ||
66 | Please make sure Shaarli has the right to write in the folder is it installed in.' | ||
67 | ); | ||
68 | } | ||
69 | } | ||
70 | |||
71 | /** | ||
72 | * Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore. | ||
73 | * ==> if user is loggedIn, merge its content with config.php, then delete options.php. | ||
74 | * | ||
75 | * @param array $config contains all configuration fields. | ||
76 | * @param bool $isLoggedIn true if user is logged in. | ||
77 | * | ||
78 | * @return void | ||
79 | */ | ||
80 | function mergeDeprecatedConfig($config, $isLoggedIn) | ||
81 | { | ||
82 | $config_file = $config['config']['CONFIG_FILE']; | ||
83 | |||
84 | if (is_file($config['config']['DATADIR'].'/options.php') && $isLoggedIn) { | ||
85 | include $config['config']['DATADIR'].'/options.php'; | ||
86 | |||
87 | // Load GLOBALS into config | ||
88 | foreach ($GLOBALS as $key => $value) { | ||
89 | $config[$key] = $value; | ||
90 | } | ||
91 | $config['config']['CONFIG_FILE'] = $config_file; | ||
92 | writeConfig($config, $isLoggedIn); | ||
93 | |||
94 | unlink($config['config']['DATADIR'].'/options.php'); | ||
95 | } | ||
96 | } | ||
97 | |||
98 | /** | ||
99 | * Exception used if a mandatory field is missing in given configuration. | ||
100 | */ | ||
101 | class MissingFieldConfigException extends Exception | ||
102 | { | ||
103 | public $field; | ||
104 | |||
105 | /** | ||
106 | * Construct exception. | ||
107 | * | ||
108 | * @param string $field field name missing. | ||
109 | */ | ||
110 | public function __construct($field) | ||
111 | { | ||
112 | $this->field = $field; | ||
113 | $this->message = 'Configuration value is required for '. $this->field; | ||
114 | } | ||
115 | } | ||
116 | |||
117 | /** | ||
118 | * Exception used if an unauthorized attempt to edit configuration has been made. | ||
119 | */ | ||
120 | class UnauthorizedConfigException extends Exception | ||
121 | { | ||
122 | /** | ||
123 | * Construct exception. | ||
124 | */ | ||
125 | public function __construct() | ||
126 | { | ||
127 | $this->message = 'You are not authorized to alter config.'; | ||
128 | } | ||
129 | } \ No newline at end of file | ||
diff --git a/application/LinkDB.php b/application/LinkDB.php index 82763618..1e16fef1 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -28,7 +28,7 @@ | |||
28 | class LinkDB implements Iterator, Countable, ArrayAccess | 28 | class LinkDB implements Iterator, Countable, ArrayAccess |
29 | { | 29 | { |
30 | // Links are stored as a PHP serialized string | 30 | // Links are stored as a PHP serialized string |
31 | private $datastore; | 31 | private $_datastore; |
32 | 32 | ||
33 | // Datastore PHP prefix | 33 | // Datastore PHP prefix |
34 | protected static $phpPrefix = '<?php /* '; | 34 | protected static $phpPrefix = '<?php /* '; |
@@ -39,23 +39,23 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
39 | // List of links (associative array) | 39 | // List of links (associative array) |
40 | // - key: link date (e.g. "20110823_124546"), | 40 | // - key: link date (e.g. "20110823_124546"), |
41 | // - value: associative array (keys: title, description...) | 41 | // - value: associative array (keys: title, description...) |
42 | private $links; | 42 | private $_links; |
43 | 43 | ||
44 | // List of all recorded URLs (key=url, value=linkdate) | 44 | // List of all recorded URLs (key=url, value=linkdate) |
45 | // for fast reserve search (url-->linkdate) | 45 | // for fast reserve search (url-->linkdate) |
46 | private $urls; | 46 | private $_urls; |
47 | 47 | ||
48 | // List of linkdate keys (for the Iterator interface implementation) | 48 | // List of linkdate keys (for the Iterator interface implementation) |
49 | private $keys; | 49 | private $_keys; |
50 | 50 | ||
51 | // Position in the $this->keys array (for the Iterator interface) | 51 | // Position in the $this->_keys array (for the Iterator interface) |
52 | private $position; | 52 | private $_position; |
53 | 53 | ||
54 | // Is the user logged in? (used to filter private links) | 54 | // Is the user logged in? (used to filter private links) |
55 | private $loggedIn; | 55 | private $_loggedIn; |
56 | 56 | ||
57 | // Hide public links | 57 | // Hide public links |
58 | private $hidePublicLinks; | 58 | private $_hidePublicLinks; |
59 | 59 | ||
60 | /** | 60 | /** |
61 | * Creates a new LinkDB | 61 | * Creates a new LinkDB |
@@ -66,11 +66,11 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
66 | */ | 66 | */ |
67 | function __construct($datastore, $isLoggedIn, $hidePublicLinks) | 67 | function __construct($datastore, $isLoggedIn, $hidePublicLinks) |
68 | { | 68 | { |
69 | $this->datastore = $datastore; | 69 | $this->_datastore = $datastore; |
70 | $this->loggedIn = $isLoggedIn; | 70 | $this->_loggedIn = $isLoggedIn; |
71 | $this->hidePublicLinks = $hidePublicLinks; | 71 | $this->_hidePublicLinks = $hidePublicLinks; |
72 | $this->checkDB(); | 72 | $this->_checkDB(); |
73 | $this->readdb(); | 73 | $this->_readDB(); |
74 | } | 74 | } |
75 | 75 | ||
76 | /** | 76 | /** |
@@ -78,7 +78,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
78 | */ | 78 | */ |
79 | public function count() | 79 | public function count() |
80 | { | 80 | { |
81 | return count($this->links); | 81 | return count($this->_links); |
82 | } | 82 | } |
83 | 83 | ||
84 | /** | 84 | /** |
@@ -87,7 +87,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
87 | public function offsetSet($offset, $value) | 87 | public function offsetSet($offset, $value) |
88 | { | 88 | { |
89 | // TODO: use exceptions instead of "die" | 89 | // TODO: use exceptions instead of "die" |
90 | if (!$this->loggedIn) { | 90 | if (!$this->_loggedIn) { |
91 | die('You are not authorized to add a link.'); | 91 | die('You are not authorized to add a link.'); |
92 | } | 92 | } |
93 | if (empty($value['linkdate']) || empty($value['url'])) { | 93 | if (empty($value['linkdate']) || empty($value['url'])) { |
@@ -96,8 +96,8 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
96 | if (empty($offset)) { | 96 | if (empty($offset)) { |
97 | die('You must specify a key.'); | 97 | die('You must specify a key.'); |
98 | } | 98 | } |
99 | $this->links[$offset] = $value; | 99 | $this->_links[$offset] = $value; |
100 | $this->urls[$value['url']]=$offset; | 100 | $this->_urls[$value['url']]=$offset; |
101 | } | 101 | } |
102 | 102 | ||
103 | /** | 103 | /** |
@@ -105,7 +105,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
105 | */ | 105 | */ |
106 | public function offsetExists($offset) | 106 | public function offsetExists($offset) |
107 | { | 107 | { |
108 | return array_key_exists($offset, $this->links); | 108 | return array_key_exists($offset, $this->_links); |
109 | } | 109 | } |
110 | 110 | ||
111 | /** | 111 | /** |
@@ -113,13 +113,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
113 | */ | 113 | */ |
114 | public function offsetUnset($offset) | 114 | public function offsetUnset($offset) |
115 | { | 115 | { |
116 | if (!$this->loggedIn) { | 116 | if (!$this->_loggedIn) { |
117 | // TODO: raise an exception | 117 | // TODO: raise an exception |
118 | die('You are not authorized to delete a link.'); | 118 | die('You are not authorized to delete a link.'); |
119 | } | 119 | } |
120 | $url = $this->links[$offset]['url']; | 120 | $url = $this->_links[$offset]['url']; |
121 | unset($this->urls[$url]); | 121 | unset($this->_urls[$url]); |
122 | unset($this->links[$offset]); | 122 | unset($this->_links[$offset]); |
123 | } | 123 | } |
124 | 124 | ||
125 | /** | 125 | /** |
@@ -127,7 +127,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
127 | */ | 127 | */ |
128 | public function offsetGet($offset) | 128 | public function offsetGet($offset) |
129 | { | 129 | { |
130 | return isset($this->links[$offset]) ? $this->links[$offset] : null; | 130 | return isset($this->_links[$offset]) ? $this->_links[$offset] : null; |
131 | } | 131 | } |
132 | 132 | ||
133 | /** | 133 | /** |
@@ -135,7 +135,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
135 | */ | 135 | */ |
136 | function current() | 136 | function current() |
137 | { | 137 | { |
138 | return $this->links[$this->keys[$this->position]]; | 138 | return $this->_links[$this->_keys[$this->_position]]; |
139 | } | 139 | } |
140 | 140 | ||
141 | /** | 141 | /** |
@@ -143,7 +143,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
143 | */ | 143 | */ |
144 | function key() | 144 | function key() |
145 | { | 145 | { |
146 | return $this->keys[$this->position]; | 146 | return $this->_keys[$this->_position]; |
147 | } | 147 | } |
148 | 148 | ||
149 | /** | 149 | /** |
@@ -151,7 +151,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
151 | */ | 151 | */ |
152 | function next() | 152 | function next() |
153 | { | 153 | { |
154 | ++$this->position; | 154 | ++$this->_position; |
155 | } | 155 | } |
156 | 156 | ||
157 | /** | 157 | /** |
@@ -161,9 +161,9 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
161 | */ | 161 | */ |
162 | function rewind() | 162 | function rewind() |
163 | { | 163 | { |
164 | $this->keys = array_keys($this->links); | 164 | $this->_keys = array_keys($this->_links); |
165 | rsort($this->keys); | 165 | rsort($this->_keys); |
166 | $this->position = 0; | 166 | $this->_position = 0; |
167 | } | 167 | } |
168 | 168 | ||
169 | /** | 169 | /** |
@@ -171,7 +171,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
171 | */ | 171 | */ |
172 | function valid() | 172 | function valid() |
173 | { | 173 | { |
174 | return isset($this->keys[$this->position]); | 174 | return isset($this->_keys[$this->_position]); |
175 | } | 175 | } |
176 | 176 | ||
177 | /** | 177 | /** |
@@ -179,14 +179,14 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
179 | * | 179 | * |
180 | * If no DB file is found, creates a dummy DB. | 180 | * If no DB file is found, creates a dummy DB. |
181 | */ | 181 | */ |
182 | private function checkDB() | 182 | private function _checkDB() |
183 | { | 183 | { |
184 | if (file_exists($this->datastore)) { | 184 | if (file_exists($this->_datastore)) { |
185 | return; | 185 | return; |
186 | } | 186 | } |
187 | 187 | ||
188 | // Create a dummy database for example | 188 | // Create a dummy database for example |
189 | $this->links = array(); | 189 | $this->_links = array(); |
190 | $link = array( | 190 | $link = array( |
191 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', | 191 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', |
192 | 'url'=>'https://github.com/shaarli/Shaarli/wiki', | 192 | 'url'=>'https://github.com/shaarli/Shaarli/wiki', |
@@ -199,7 +199,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
199 | 'linkdate'=> date('Ymd_His'), | 199 | 'linkdate'=> date('Ymd_His'), |
200 | 'tags'=>'opensource software' | 200 | 'tags'=>'opensource software' |
201 | ); | 201 | ); |
202 | $this->links[$link['linkdate']] = $link; | 202 | $this->_links[$link['linkdate']] = $link; |
203 | 203 | ||
204 | $link = array( | 204 | $link = array( |
205 | 'title'=>'My secret stuff... - Pastebin.com', | 205 | 'title'=>'My secret stuff... - Pastebin.com', |
@@ -209,60 +209,60 @@ You use the community supported version of the original Shaarli project, by Seba | |||
209 | 'linkdate'=> date('Ymd_His', strtotime('-1 minute')), | 209 | 'linkdate'=> date('Ymd_His', strtotime('-1 minute')), |
210 | 'tags'=>'secretstuff' | 210 | 'tags'=>'secretstuff' |
211 | ); | 211 | ); |
212 | $this->links[$link['linkdate']] = $link; | 212 | $this->_links[$link['linkdate']] = $link; |
213 | 213 | ||
214 | // Write database to disk | 214 | // Write database to disk |
215 | // TODO: raise an exception if the file is not write-able | 215 | // TODO: raise an exception if the file is not write-able |
216 | file_put_contents( | 216 | file_put_contents( |
217 | $this->datastore, | 217 | $this->_datastore, |
218 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix | 218 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix |
219 | ); | 219 | ); |
220 | } | 220 | } |
221 | 221 | ||
222 | /** | 222 | /** |
223 | * Reads database from disk to memory | 223 | * Reads database from disk to memory |
224 | */ | 224 | */ |
225 | private function readdb() | 225 | private function _readDB() |
226 | { | 226 | { |
227 | 227 | ||
228 | // Public links are hidden and user not logged in => nothing to show | 228 | // Public links are hidden and user not logged in => nothing to show |
229 | if ($this->hidePublicLinks && !$this->loggedIn) { | 229 | if ($this->_hidePublicLinks && !$this->_loggedIn) { |
230 | $this->links = array(); | 230 | $this->_links = array(); |
231 | return; | 231 | return; |
232 | } | 232 | } |
233 | 233 | ||
234 | // Read data | 234 | // Read data |
235 | // Note that gzinflate is faster than gzuncompress. | 235 | // Note that gzinflate is faster than gzuncompress. |
236 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | 236 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 |
237 | $this->links = array(); | 237 | $this->_links = array(); |
238 | 238 | ||
239 | if (file_exists($this->datastore)) { | 239 | if (file_exists($this->_datastore)) { |
240 | $this->links = unserialize(gzinflate(base64_decode( | 240 | $this->_links = unserialize(gzinflate(base64_decode( |
241 | substr(file_get_contents($this->datastore), | 241 | substr(file_get_contents($this->_datastore), |
242 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | 242 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); |
243 | } | 243 | } |
244 | 244 | ||
245 | // If user is not logged in, filter private links. | 245 | // If user is not logged in, filter private links. |
246 | if (!$this->loggedIn) { | 246 | if (!$this->_loggedIn) { |
247 | $toremove = array(); | 247 | $toremove = array(); |
248 | foreach ($this->links as $link) { | 248 | foreach ($this->_links as $link) { |
249 | if ($link['private'] != 0) { | 249 | if ($link['private'] != 0) { |
250 | $toremove[] = $link['linkdate']; | 250 | $toremove[] = $link['linkdate']; |
251 | } | 251 | } |
252 | } | 252 | } |
253 | foreach ($toremove as $linkdate) { | 253 | foreach ($toremove as $linkdate) { |
254 | unset($this->links[$linkdate]); | 254 | unset($this->_links[$linkdate]); |
255 | } | 255 | } |
256 | } | 256 | } |
257 | 257 | ||
258 | // Keep the list of the mapping URLs-->linkdate up-to-date. | 258 | // Keep the list of the mapping URLs-->linkdate up-to-date. |
259 | $this->urls = array(); | 259 | $this->_urls = array(); |
260 | foreach ($this->links as $link) { | 260 | foreach ($this->_links as $link) { |
261 | $this->urls[$link['url']] = $link['linkdate']; | 261 | $this->_urls[$link['url']] = $link['linkdate']; |
262 | } | 262 | } |
263 | 263 | ||
264 | // Escape links data | 264 | // Escape links data |
265 | foreach($this->links as &$link) { | 265 | foreach($this->_links as &$link) { |
266 | sanitizeLink($link); | 266 | sanitizeLink($link); |
267 | } | 267 | } |
268 | } | 268 | } |
@@ -272,13 +272,13 @@ You use the community supported version of the original Shaarli project, by Seba | |||
272 | */ | 272 | */ |
273 | public function savedb() | 273 | public function savedb() |
274 | { | 274 | { |
275 | if (!$this->loggedIn) { | 275 | if (!$this->_loggedIn) { |
276 | // TODO: raise an Exception instead | 276 | // TODO: raise an Exception instead |
277 | die('You are not authorized to change the database.'); | 277 | die('You are not authorized to change the database.'); |
278 | } | 278 | } |
279 | file_put_contents( | 279 | file_put_contents( |
280 | $this->datastore, | 280 | $this->_datastore, |
281 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix | 281 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix |
282 | ); | 282 | ); |
283 | invalidateCaches(); | 283 | invalidateCaches(); |
284 | } | 284 | } |
@@ -288,8 +288,8 @@ You use the community supported version of the original Shaarli project, by Seba | |||
288 | */ | 288 | */ |
289 | public function getLinkFromUrl($url) | 289 | public function getLinkFromUrl($url) |
290 | { | 290 | { |
291 | if (isset($this->urls[$url])) { | 291 | if (isset($this->_urls[$url])) { |
292 | return $this->links[$this->urls[$url]]; | 292 | return $this->_links[$this->_urls[$url]]; |
293 | } | 293 | } |
294 | return false; | 294 | return false; |
295 | } | 295 | } |
@@ -316,7 +316,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
316 | $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8'); | 316 | $search = mb_convert_case($searchterms, MB_CASE_LOWER, 'UTF-8'); |
317 | $keys = array('title', 'description', 'url', 'tags'); | 317 | $keys = array('title', 'description', 'url', 'tags'); |
318 | 318 | ||
319 | foreach ($this->links as $link) { | 319 | foreach ($this->_links as $link) { |
320 | $found = false; | 320 | $found = false; |
321 | 321 | ||
322 | foreach ($keys as $key) { | 322 | foreach ($keys as $key) { |
@@ -352,7 +352,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
352 | $searchtags = explode(' ', $t); | 352 | $searchtags = explode(' ', $t); |
353 | $filtered = array(); | 353 | $filtered = array(); |
354 | 354 | ||
355 | foreach ($this->links as $l) { | 355 | foreach ($this->_links as $l) { |
356 | $linktags = explode( | 356 | $linktags = explode( |
357 | ' ', | 357 | ' ', |
358 | ($casesensitive ? $l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8')) | 358 | ($casesensitive ? $l['tags']:mb_convert_case($l['tags'], MB_CASE_LOWER, 'UTF-8')) |
@@ -380,7 +380,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
380 | } | 380 | } |
381 | 381 | ||
382 | $filtered = array(); | 382 | $filtered = array(); |
383 | foreach ($this->links as $l) { | 383 | foreach ($this->_links as $l) { |
384 | if (startsWith($l['linkdate'], $day)) { | 384 | if (startsWith($l['linkdate'], $day)) { |
385 | $filtered[$l['linkdate']] = $l; | 385 | $filtered[$l['linkdate']] = $l; |
386 | } | 386 | } |
@@ -395,7 +395,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
395 | public function filterSmallHash($smallHash) | 395 | public function filterSmallHash($smallHash) |
396 | { | 396 | { |
397 | $filtered = array(); | 397 | $filtered = array(); |
398 | foreach ($this->links as $l) { | 398 | foreach ($this->_links as $l) { |
399 | if ($smallHash == smallHash($l['linkdate'])) { | 399 | if ($smallHash == smallHash($l['linkdate'])) { |
400 | // Yes, this is ugly and slow | 400 | // Yes, this is ugly and slow |
401 | $filtered[$l['linkdate']] = $l; | 401 | $filtered[$l['linkdate']] = $l; |
@@ -412,7 +412,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
412 | public function allTags() | 412 | public function allTags() |
413 | { | 413 | { |
414 | $tags = array(); | 414 | $tags = array(); |
415 | foreach ($this->links as $link) { | 415 | foreach ($this->_links as $link) { |
416 | foreach (explode(' ', $link['tags']) as $tag) { | 416 | foreach (explode(' ', $link['tags']) as $tag) { |
417 | if (!empty($tag)) { | 417 | if (!empty($tag)) { |
418 | $tags[$tag] = (empty($tags[$tag]) ? 1 : $tags[$tag] + 1); | 418 | $tags[$tag] = (empty($tags[$tag]) ? 1 : $tags[$tag] + 1); |
@@ -431,7 +431,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
431 | public function days() | 431 | public function days() |
432 | { | 432 | { |
433 | $linkDays = array(); | 433 | $linkDays = array(); |
434 | foreach (array_keys($this->links) as $day) { | 434 | foreach (array_keys($this->_links) as $day) { |
435 | $linkDays[substr($day, 0, 8)] = 0; | 435 | $linkDays[substr($day, 0, 8)] = 0; |
436 | } | 436 | } |
437 | $linkDays = array_keys($linkDays); | 437 | $linkDays = array_keys($linkDays); |
@@ -11,7 +11,8 @@ | |||
11 | date_default_timezone_set('UTC'); | 11 | date_default_timezone_set('UTC'); |
12 | 12 | ||
13 | // ----------------------------------------------------------------------------------------------- | 13 | // ----------------------------------------------------------------------------------------------- |
14 | // Hardcoded parameter (These parameters can be overwritten by creating the file /data/options.php) | 14 | // Hardcoded parameter (These parameters can be overwritten by editing the file /data/config.php) |
15 | // You should not touch any code below (or at your own risks!) | ||
15 | $GLOBALS['config']['DATADIR'] = 'data'; // Data subdirectory | 16 | $GLOBALS['config']['DATADIR'] = 'data'; // Data subdirectory |
16 | $GLOBALS['config']['CONFIG_FILE'] = $GLOBALS['config']['DATADIR'].'/config.php'; // Configuration file (user login/password) | 17 | $GLOBALS['config']['CONFIG_FILE'] = $GLOBALS['config']['DATADIR'].'/config.php'; // Configuration file (user login/password) |
17 | $GLOBALS['config']['DATASTORE'] = $GLOBALS['config']['DATADIR'].'/datastore.php'; // Data storage file. | 18 | $GLOBALS['config']['DATASTORE'] = $GLOBALS['config']['DATADIR'].'/datastore.php'; // Data storage file. |
@@ -36,10 +37,6 @@ $GLOBALS['config']['ARCHIVE_ORG'] = false; // For each link, add a link to an ar | |||
36 | $GLOBALS['config']['ENABLE_RSS_PERMALINKS'] = true; // Enable RSS permalinks by default. This corresponds to the default behavior of shaarli before this was added as an option. | 37 | $GLOBALS['config']['ENABLE_RSS_PERMALINKS'] = true; // Enable RSS permalinks by default. This corresponds to the default behavior of shaarli before this was added as an option. |
37 | $GLOBALS['config']['HIDE_PUBLIC_LINKS'] = false; | 38 | $GLOBALS['config']['HIDE_PUBLIC_LINKS'] = false; |
38 | // ----------------------------------------------------------------------------------------------- | 39 | // ----------------------------------------------------------------------------------------------- |
39 | // You should not touch below (or at your own risks!) | ||
40 | // Optional config file. | ||
41 | if (is_file($GLOBALS['config']['DATADIR'].'/options.php')) require($GLOBALS['config']['DATADIR'].'/options.php'); | ||
42 | |||
43 | define('shaarli_version','0.0.45beta'); | 40 | define('shaarli_version','0.0.45beta'); |
44 | // http://server.com/x/shaarli --> /shaarli/ | 41 | // http://server.com/x/shaarli --> /shaarli/ |
45 | define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); | 42 | define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); |
@@ -66,9 +63,15 @@ checkphpversion(); | |||
66 | error_reporting(E_ALL^E_WARNING); // See all error except warnings. | 63 | error_reporting(E_ALL^E_WARNING); // See all error except warnings. |
67 | //error_reporting(-1); // See all errors (for debugging only) | 64 | //error_reporting(-1); // See all errors (for debugging only) |
68 | 65 | ||
66 | // User configuration | ||
67 | if (is_file($GLOBALS['config']['CONFIG_FILE'])) { | ||
68 | require_once $GLOBALS['config']['CONFIG_FILE']; | ||
69 | } | ||
70 | |||
69 | // Shaarli library | 71 | // Shaarli library |
70 | require_once 'application/LinkDB.php'; | 72 | require_once 'application/LinkDB.php'; |
71 | require_once 'application/Utils.php'; | 73 | require_once 'application/Utils.php'; |
74 | require_once 'application/Config.php'; | ||
72 | 75 | ||
73 | include "inc/rain.tpl.class.php"; //include Rain TPL | 76 | include "inc/rain.tpl.class.php"; //include Rain TPL |
74 | raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory | 77 | raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory |
@@ -100,15 +103,15 @@ if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.escape(indexU | |||
100 | if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get(); | 103 | if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get(); |
101 | if (empty($GLOBALS['redirector'])) $GLOBALS['redirector']=''; | 104 | if (empty($GLOBALS['redirector'])) $GLOBALS['redirector']=''; |
102 | if (empty($GLOBALS['disablesessionprotection'])) $GLOBALS['disablesessionprotection']=false; | 105 | if (empty($GLOBALS['disablesessionprotection'])) $GLOBALS['disablesessionprotection']=false; |
103 | if (empty($GLOBALS['disablejquery'])) $GLOBALS['disablejquery']=false; | ||
104 | if (empty($GLOBALS['privateLinkByDefault'])) $GLOBALS['privateLinkByDefault']=false; | 106 | if (empty($GLOBALS['privateLinkByDefault'])) $GLOBALS['privateLinkByDefault']=false; |
105 | if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?'; | 107 | if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?'; |
106 | // I really need to rewrite Shaarli with a proper configuation manager. | 108 | // I really need to rewrite Shaarli with a proper configuation manager. |
107 | 109 | ||
108 | // Run config screen if first run: | 110 | // Run config screen if first run: |
109 | if (!is_file($GLOBALS['config']['CONFIG_FILE'])) install(); | 111 | if (! is_file($GLOBALS['config']['CONFIG_FILE'])) { |
112 | install(); | ||
113 | } | ||
110 | 114 | ||
111 | require $GLOBALS['config']['CONFIG_FILE']; // Read login/password hash into $GLOBALS. | ||
112 | $GLOBALS['title'] = !empty($GLOBALS['title']) ? escape($GLOBALS['title']) : ''; | 115 | $GLOBALS['title'] = !empty($GLOBALS['title']) ? escape($GLOBALS['title']) : ''; |
113 | $GLOBALS['titleLink'] = !empty($GLOBALS['titleLink']) ? escape($GLOBALS['titleLink']) : ''; | 116 | $GLOBALS['titleLink'] = !empty($GLOBALS['titleLink']) ? escape($GLOBALS['titleLink']) : ''; |
114 | $GLOBALS['redirector'] = !empty($GLOBALS['redirector']) ? escape($GLOBALS['redirector']) : ''; | 117 | $GLOBALS['redirector'] = !empty($GLOBALS['redirector']) ? escape($GLOBALS['redirector']) : ''; |
@@ -856,15 +859,18 @@ function showATOM() | |||
856 | // Daily RSS feed: 1 RSS entry per day giving all the links on that day. | 859 | // Daily RSS feed: 1 RSS entry per day giving all the links on that day. |
857 | // Gives the last 7 days (which have links). | 860 | // Gives the last 7 days (which have links). |
858 | // This RSS feed cannot be filtered. | 861 | // This RSS feed cannot be filtered. |
859 | function showDailyRSS() | 862 | function showDailyRSS() { |
860 | { | ||
861 | // Cache system | 863 | // Cache system |
862 | $query = $_SERVER["QUERY_STRING"]; | 864 | $query = $_SERVER["QUERY_STRING"]; |
863 | $cache = new pageCache(pageUrl(),startsWith($query,'do=dailyrss') && !isLoggedIn()); | 865 | $cache = new pageCache(pageUrl(), startsWith($query, 'do=dailyrss') && !isLoggedIn()); |
864 | $cached = $cache->cachedVersion(); if (!empty($cached)) { echo $cached; exit; } | 866 | $cached = $cache->cachedVersion(); |
865 | // If cached was not found (or not usable), then read the database and build the response: | 867 | if (!empty($cached)) { |
868 | echo $cached; | ||
869 | exit; | ||
870 | } | ||
866 | 871 | ||
867 | // Read links from database (and filter private links if used it not logged in). | 872 | // If cached was not found (or not usable), then read the database and build the response: |
873 | // Read links from database (and filter private links if used it not logged in). | ||
868 | $LINKSDB = new LinkDB( | 874 | $LINKSDB = new LinkDB( |
869 | $GLOBALS['config']['DATASTORE'], | 875 | $GLOBALS['config']['DATASTORE'], |
870 | isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'], | 876 | isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'], |
@@ -874,60 +880,75 @@ function showDailyRSS() | |||
874 | /* Some Shaarlies may have very few links, so we need to look | 880 | /* Some Shaarlies may have very few links, so we need to look |
875 | back in time (rsort()) until we have enough days ($nb_of_days). | 881 | back in time (rsort()) until we have enough days ($nb_of_days). |
876 | */ | 882 | */ |
877 | $linkdates=array(); foreach($LINKSDB as $linkdate=>$value) { $linkdates[]=$linkdate; } | 883 | $linkdates = array(); |
884 | foreach ($LINKSDB as $linkdate => $value) { | ||
885 | $linkdates[] = $linkdate; | ||
886 | } | ||
878 | rsort($linkdates); | 887 | rsort($linkdates); |
879 | $nb_of_days=7; // We take 7 days. | 888 | $nb_of_days = 7; // We take 7 days. |
880 | $today=Date('Ymd'); | 889 | $today = Date('Ymd'); |
881 | $days=array(); | 890 | $days = array(); |
882 | foreach($linkdates as $linkdate) | 891 | |
883 | { | 892 | foreach ($linkdates as $linkdate) { |
884 | $day=substr($linkdate,0,8); // Extract day (without time) | 893 | $day = substr($linkdate, 0, 8); // Extract day (without time) |
885 | if (strcmp($day,$today)<0) | 894 | if (strcmp($day,$today) < 0) { |
886 | { | 895 | if (empty($days[$day])) { |
887 | if (empty($days[$day])) $days[$day]=array(); | 896 | $days[$day] = array(); |
888 | $days[$day][]=$linkdate; | 897 | } |
898 | $days[$day][] = $linkdate; | ||
899 | } | ||
900 | |||
901 | if (count($days) > $nb_of_days) { | ||
902 | break; // Have we collected enough days? | ||
889 | } | 903 | } |
890 | if (count($days)>$nb_of_days) break; // Have we collected enough days? | ||
891 | } | 904 | } |
892 | 905 | ||
893 | // Build the RSS feed. | 906 | // Build the RSS feed. |
894 | header('Content-Type: application/rss+xml; charset=utf-8'); | 907 | header('Content-Type: application/rss+xml; charset=utf-8'); |
895 | $pageaddr=escape(indexUrl()); | 908 | $pageaddr = escape(indexUrl()); |
896 | echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">'; | 909 | echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">'; |
897 | echo '<channel><title>Daily - '.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>'; | 910 | echo '<channel>'; |
898 | echo '<description>Daily shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n"; | 911 | echo '<title>Daily - '. $GLOBALS['title'] . '</title>'; |
899 | 912 | echo '<link>'. $pageaddr .'</link>'; | |
900 | foreach($days as $day=>$linkdates) // For each day. | 913 | echo '<description>Daily shared links</description>'; |
901 | { | 914 | echo '<language>en-en</language>'; |
902 | $daydate = utf8_encode(strftime('%A %d, %B %Y',linkdate2timestamp($day.'_000000'))); // Full text date | 915 | echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL; |
916 | |||
917 | // For each day. | ||
918 | foreach ($days as $day => $linkdates) { | ||
919 | $daydate = linkdate2timestamp($day.'_000000'); // Full text date | ||
903 | $rfc822date = linkdate2rfc822($day.'_000000'); | 920 | $rfc822date = linkdate2rfc822($day.'_000000'); |
904 | $absurl=escape(indexUrl().'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page. | 921 | $absurl = escape(indexUrl().'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page. |
905 | echo '<item><title>'.$GLOBALS['title'].' - '.$daydate.'</title><guid>'.$absurl.'</guid><link>'.$absurl.'</link>'; | ||
906 | echo '<pubDate>'.escape($rfc822date)."</pubDate>"; | ||
907 | 922 | ||
908 | // Build the HTML body of this RSS entry. | 923 | // Build the HTML body of this RSS entry. |
909 | $html=''; | 924 | $html = ''; |
910 | $href=''; | 925 | $href = ''; |
911 | $links=array(); | 926 | $links = array(); |
927 | |||
912 | // We pre-format some fields for proper output. | 928 | // We pre-format some fields for proper output. |
913 | foreach($linkdates as $linkdate) | 929 | foreach ($linkdates as $linkdate) { |
914 | { | ||
915 | $l = $LINKSDB[$linkdate]; | 930 | $l = $LINKSDB[$linkdate]; |
916 | $l['formatedDescription']=nl2br(keepMultipleSpaces(text2clickable($l['description']))); | 931 | $l['formatedDescription'] = nl2br(keepMultipleSpaces(text2clickable($l['description']))); |
917 | $l['thumbnail'] = thumbnail($l['url']); | 932 | $l['thumbnail'] = thumbnail($l['url']); |
918 | $l['timestamp'] = linkdate2timestamp($l['linkdate']); | 933 | $l['timestamp'] = linkdate2timestamp($l['linkdate']); |
919 | if (startsWith($l['url'],'?')) $l['url']=indexUrl().$l['url']; // make permalink URL absolute | 934 | if (startsWith($l['url'], '?')) { |
920 | $links[$linkdate]=$l; | 935 | $l['url'] = indexUrl() . $l['url']; // make permalink URL absolute |
936 | } | ||
937 | $links[$linkdate] = $l; | ||
921 | } | 938 | } |
939 | |||
922 | // Then build the HTML for this day: | 940 | // Then build the HTML for this day: |
923 | $tpl = new RainTPL; | 941 | $tpl = new RainTPL; |
924 | $tpl->assign('links',$links); | 942 | $tpl->assign('title', $GLOBALS['title']); |
925 | $html = $tpl->draw('dailyrss',$return_string=true); | 943 | $tpl->assign('daydate', $daydate); |
926 | echo "\n"; | 944 | $tpl->assign('absurl', $absurl); |
927 | echo '<description><![CDATA['.$html.']]></description>'."\n</item>\n\n"; | 945 | $tpl->assign('links', $links); |
946 | $tpl->assign('rfc822date', escape($rfc822date)); | ||
947 | $html = $tpl->draw('dailyrss', $return_string=true); | ||
928 | 948 | ||
949 | echo $html . PHP_EOL; | ||
929 | } | 950 | } |
930 | echo '</channel></rss><!-- Cached version of '.escape(pageUrl()).' -->'; | 951 | echo '</channel></rss><!-- Cached version of '. escape(pageUrl()) .' -->'; |
931 | 952 | ||
932 | $cache->cache(ob_get_contents()); | 953 | $cache->cache(ob_get_contents()); |
933 | ob_end_flush(); | 954 | ob_end_flush(); |
@@ -1106,7 +1127,11 @@ function renderPage() | |||
1106 | 1127 | ||
1107 | // Check if this tag is already in the search query and ignore it if it is. | 1128 | // Check if this tag is already in the search query and ignore it if it is. |
1108 | // Each tag is always separated by a space | 1129 | // Each tag is always separated by a space |
1109 | $current_tags = explode(' ', $params['searchtags']); | 1130 | if (isset($params['searchtags'])) { |
1131 | $current_tags = explode(' ', $params['searchtags']); | ||
1132 | } else { | ||
1133 | $current_tags = array(); | ||
1134 | } | ||
1110 | $addtag = true; | 1135 | $addtag = true; |
1111 | foreach ($current_tags as $value) { | 1136 | foreach ($current_tags as $value) { |
1112 | if ($value === $_GET['addtag']) { | 1137 | if ($value === $_GET['addtag']) { |
@@ -1229,7 +1254,19 @@ function renderPage() | |||
1229 | // Save new password | 1254 | // Save new password |
1230 | $GLOBALS['salt'] = sha1(uniqid('',true).'_'.mt_rand()); // Salt renders rainbow-tables attacks useless. | 1255 | $GLOBALS['salt'] = sha1(uniqid('',true).'_'.mt_rand()); // Salt renders rainbow-tables attacks useless. |
1231 | $GLOBALS['hash'] = sha1($_POST['setpassword'].$GLOBALS['login'].$GLOBALS['salt']); | 1256 | $GLOBALS['hash'] = sha1($_POST['setpassword'].$GLOBALS['login'].$GLOBALS['salt']); |
1232 | writeConfig(); | 1257 | try { |
1258 | writeConfig($GLOBALS, isLoggedIn()); | ||
1259 | } | ||
1260 | catch(Exception $e) { | ||
1261 | error_log( | ||
1262 | 'ERROR while writing config file after changing password.' . PHP_EOL . | ||
1263 | $e->getMessage() | ||
1264 | ); | ||
1265 | |||
1266 | // TODO: do not handle exceptions/errors in JS. | ||
1267 | echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>'; | ||
1268 | exit; | ||
1269 | } | ||
1233 | echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>'; | 1270 | echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>'; |
1234 | exit; | 1271 | exit; |
1235 | } | 1272 | } |
@@ -1258,12 +1295,23 @@ function renderPage() | |||
1258 | $GLOBALS['titleLink']=$_POST['titleLink']; | 1295 | $GLOBALS['titleLink']=$_POST['titleLink']; |
1259 | $GLOBALS['redirector']=$_POST['redirector']; | 1296 | $GLOBALS['redirector']=$_POST['redirector']; |
1260 | $GLOBALS['disablesessionprotection']=!empty($_POST['disablesessionprotection']); | 1297 | $GLOBALS['disablesessionprotection']=!empty($_POST['disablesessionprotection']); |
1261 | $GLOBALS['disablejquery']=!empty($_POST['disablejquery']); | ||
1262 | $GLOBALS['privateLinkByDefault']=!empty($_POST['privateLinkByDefault']); | 1298 | $GLOBALS['privateLinkByDefault']=!empty($_POST['privateLinkByDefault']); |
1263 | $GLOBALS['config']['ENABLE_RSS_PERMALINKS']= !empty($_POST['enableRssPermalinks']); | 1299 | $GLOBALS['config']['ENABLE_RSS_PERMALINKS']= !empty($_POST['enableRssPermalinks']); |
1264 | $GLOBALS['config']['ENABLE_UPDATECHECK'] = !empty($_POST['updateCheck']); | 1300 | $GLOBALS['config']['ENABLE_UPDATECHECK'] = !empty($_POST['updateCheck']); |
1265 | $GLOBALS['config']['HIDE_PUBLIC_LINKS'] = !empty($_POST['hidePublicLinks']); | 1301 | $GLOBALS['config']['HIDE_PUBLIC_LINKS'] = !empty($_POST['hidePublicLinks']); |
1266 | writeConfig(); | 1302 | try { |
1303 | writeConfig($GLOBALS, isLoggedIn()); | ||
1304 | } | ||
1305 | catch(Exception $e) { | ||
1306 | error_log( | ||
1307 | 'ERROR while writing config file after configuration update.' . PHP_EOL . | ||
1308 | $e->getMessage() | ||
1309 | ); | ||
1310 | |||
1311 | // TODO: do not handle exceptions/errors in JS. | ||
1312 | echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>'; | ||
1313 | exit; | ||
1314 | } | ||
1267 | echo '<script>alert("Configuration was saved.");document.location=\'?do=tools\';</script>'; | 1315 | echo '<script>alert("Configuration was saved.");document.location=\'?do=tools\';</script>'; |
1268 | exit; | 1316 | exit; |
1269 | } | 1317 | } |
@@ -1345,6 +1393,7 @@ function renderPage() | |||
1345 | { | 1393 | { |
1346 | if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! | 1394 | if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! |
1347 | $tags = trim(preg_replace('/\s\s+/',' ', $_POST['lf_tags'])); // Remove multiple spaces. | 1395 | $tags = trim(preg_replace('/\s\s+/',' ', $_POST['lf_tags'])); // Remove multiple spaces. |
1396 | $tags = implode(' ', array_unique(explode(' ', $tags))); // Remove duplicates. | ||
1348 | $linkdate=$_POST['lf_linkdate']; | 1397 | $linkdate=$_POST['lf_linkdate']; |
1349 | $url = trim($_POST['lf_url']); | 1398 | $url = trim($_POST['lf_url']); |
1350 | if (!startsWith($url,'http:') && !startsWith($url,'https:') && !startsWith($url,'ftp:') && !startsWith($url,'magnet:') && !startsWith($url,'?') && !startsWith($url,'javascript:')) | 1399 | if (!startsWith($url,'http:') && !startsWith($url,'https:') && !startsWith($url,'ftp:') && !startsWith($url,'magnet:') && !startsWith($url,'?') && !startsWith($url,'javascript:')) |
@@ -1714,7 +1763,7 @@ function buildLinkList($PAGE,$LINKSDB) | |||
1714 | { | 1763 | { |
1715 | header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); | 1764 | header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); |
1716 | echo '<h1>404 Not found.</h1>Oh crap. The link you are trying to reach does not exist or has been deleted.'; | 1765 | echo '<h1>404 Not found.</h1>Oh crap. The link you are trying to reach does not exist or has been deleted.'; |
1717 | echo '<br>You would mind <a href="?">clicking here</a>?'; | 1766 | echo '<br>Would you mind <a href="?">clicking here</a>?'; |
1718 | exit; | 1767 | exit; |
1719 | } | 1768 | } |
1720 | $search_type='permalink'; | 1769 | $search_type='permalink'; |
@@ -2020,7 +2069,19 @@ function install() | |||
2020 | $GLOBALS['hash'] = sha1($_POST['setpassword'].$GLOBALS['login'].$GLOBALS['salt']); | 2069 | $GLOBALS['hash'] = sha1($_POST['setpassword'].$GLOBALS['login'].$GLOBALS['salt']); |
2021 | $GLOBALS['title'] = (empty($_POST['title']) ? 'Shared links on '.escape(indexUrl()) : $_POST['title'] ); | 2070 | $GLOBALS['title'] = (empty($_POST['title']) ? 'Shared links on '.escape(indexUrl()) : $_POST['title'] ); |
2022 | $GLOBALS['config']['ENABLE_UPDATECHECK'] = !empty($_POST['updateCheck']); | 2071 | $GLOBALS['config']['ENABLE_UPDATECHECK'] = !empty($_POST['updateCheck']); |
2023 | writeConfig(); | 2072 | try { |
2073 | writeConfig($GLOBALS, isLoggedIn()); | ||
2074 | } | ||
2075 | catch(Exception $e) { | ||
2076 | error_log( | ||
2077 | 'ERROR while writing config file after installation.' . PHP_EOL . | ||
2078 | $e->getMessage() | ||
2079 | ); | ||
2080 | |||
2081 | // TODO: do not handle exceptions/errors in JS. | ||
2082 | echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>'; | ||
2083 | exit; | ||
2084 | } | ||
2024 | echo '<script>alert("Shaarli is now configured. Please enter your login/password and start shaaring your links!");document.location=\'?do=login\';</script>'; | 2085 | echo '<script>alert("Shaarli is now configured. Please enter your login/password and start shaaring your links!");document.location=\'?do=login\';</script>'; |
2025 | exit; | 2086 | exit; |
2026 | } | 2087 | } |
@@ -2134,30 +2195,7 @@ if (!function_exists('json_encode')) { | |||
2134 | } | 2195 | } |
2135 | } | 2196 | } |
2136 | 2197 | ||
2137 | // Re-write configuration file according to globals. | 2198 | |
2138 | // Requires some $GLOBALS to be set (login,hash,salt,title). | ||
2139 | // If the config file cannot be saved, an error message is displayed and the user is redirected to "Tools" menu. | ||
2140 | // (otherwise, the function simply returns.) | ||
2141 | function writeConfig() | ||
2142 | { | ||
2143 | if (is_file($GLOBALS['config']['CONFIG_FILE']) && !isLoggedIn()) die('You are not authorized to alter config.'); // Only logged in user can alter config. | ||
2144 | $config='<?php $GLOBALS[\'login\']='.var_export($GLOBALS['login'],true).'; $GLOBALS[\'hash\']='.var_export($GLOBALS['hash'],true).'; $GLOBALS[\'salt\']='.var_export($GLOBALS['salt'],true).'; '; | ||
2145 | $config .='$GLOBALS[\'timezone\']='.var_export($GLOBALS['timezone'],true).'; date_default_timezone_set('.var_export($GLOBALS['timezone'],true).'); $GLOBALS[\'title\']='.var_export($GLOBALS['title'],true).';'; | ||
2146 | $config .= '$GLOBALS[\'titleLink\']='.var_export($GLOBALS['titleLink'],true).'; '; | ||
2147 | $config .= '$GLOBALS[\'redirector\']='.var_export($GLOBALS['redirector'],true).'; '; | ||
2148 | $config .= '$GLOBALS[\'disablesessionprotection\']='.var_export($GLOBALS['disablesessionprotection'],true).'; '; | ||
2149 | $config .= '$GLOBALS[\'disablejquery\']='.var_export($GLOBALS['disablejquery'],true).'; '; | ||
2150 | $config .= '$GLOBALS[\'privateLinkByDefault\']='.var_export($GLOBALS['privateLinkByDefault'],true).'; '; | ||
2151 | $config .= '$GLOBALS[\'config\'][\'ENABLE_RSS_PERMALINKS\']='.var_export($GLOBALS['config']['ENABLE_RSS_PERMALINKS'], true).'; '; | ||
2152 | $config .= '$GLOBALS[\'config\'][\'ENABLE_UPDATECHECK\']='.var_export($GLOBALS['config']['ENABLE_UPDATECHECK'], true).'; '; | ||
2153 | $config .= '$GLOBALS[\'config\'][\'HIDE_PUBLIC_LINKS\']='.var_export($GLOBALS['config']['HIDE_PUBLIC_LINKS'], true).'; '; | ||
2154 | $config .= ' ?>'; | ||
2155 | if (!file_put_contents($GLOBALS['config']['CONFIG_FILE'],$config) || strcmp(file_get_contents($GLOBALS['config']['CONFIG_FILE']),$config)!=0) | ||
2156 | { | ||
2157 | echo '<script>alert("Shaarli could not create the config file. Please make sure Shaarli has the right to write in the folder is it installed in.");document.location=\'?\';</script>'; | ||
2158 | exit; | ||
2159 | } | ||
2160 | } | ||
2161 | 2199 | ||
2162 | /* Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL, | 2200 | /* Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL, |
2163 | I have deported the thumbnail URL code generation here, otherwise this would slow down page generation. | 2201 | I have deported the thumbnail URL code generation here, otherwise this would slow down page generation. |
@@ -2386,6 +2424,15 @@ function invalidateCaches() | |||
2386 | pageCache::purgeCache(); // Purge page cache shared by sessions. | 2424 | pageCache::purgeCache(); // Purge page cache shared by sessions. |
2387 | } | 2425 | } |
2388 | 2426 | ||
2427 | try { | ||
2428 | mergeDeprecatedConfig($GLOBALS, isLoggedIn()); | ||
2429 | } catch(Exception $e) { | ||
2430 | error_log( | ||
2431 | 'ERROR while merging deprecated options.php file.' . PHP_EOL . | ||
2432 | $e->getMessage() | ||
2433 | ); | ||
2434 | } | ||
2435 | |||
2389 | if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=genthumbnail')) { genThumbnail(); exit; } // Thumbnail generation/cache does not need the link database. | 2436 | if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=genthumbnail')) { genThumbnail(); exit; } // Thumbnail generation/cache does not need the link database. |
2390 | if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; } | 2437 | if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; } |
2391 | if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=atom')) { showATOM(); exit; } | 2438 | if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=atom')) { showATOM(); exit; } |
diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php new file mode 100755 index 00000000..4279c57e --- /dev/null +++ b/tests/ConfigTest.php | |||
@@ -0,0 +1,177 @@ | |||
1 | <?php | ||
2 | /** | ||
3 | * Config' tests | ||
4 | */ | ||
5 | |||
6 | require_once 'application/Config.php'; | ||
7 | |||
8 | /** | ||
9 | * Unitary tests for Shaarli config related functions | ||
10 | */ | ||
11 | class ConfigTest extends PHPUnit_Framework_TestCase | ||
12 | { | ||
13 | // Configuration input set. | ||
14 | private static $_configFields; | ||
15 | |||
16 | /** | ||
17 | * Executed before each test. | ||
18 | */ | ||
19 | public function setUp() | ||
20 | { | ||
21 | self::$_configFields = [ | ||
22 | 'login' => 'login', | ||
23 | 'hash' => 'hash', | ||
24 | 'salt' => 'salt', | ||
25 | 'timezone' => 'Europe/Paris', | ||
26 | 'title' => 'title', | ||
27 | 'titleLink' => 'titleLink', | ||
28 | 'redirector' => '', | ||
29 | 'disablesessionprotection' => false, | ||
30 | 'privateLinkByDefault' => false, | ||
31 | 'config' => [ | ||
32 | 'CONFIG_FILE' => 'tests/config.php', | ||
33 | 'DATADIR' => 'tests', | ||
34 | 'config1' => 'config1data', | ||
35 | 'config2' => 'config2data', | ||
36 | ] | ||
37 | ]; | ||
38 | } | ||
39 | |||
40 | /** | ||
41 | * Executed after each test. | ||
42 | * | ||
43 | * @return void | ||
44 | */ | ||
45 | public function tearDown() | ||
46 | { | ||
47 | if (is_file(self::$_configFields['config']['CONFIG_FILE'])) { | ||
48 | unlink(self::$_configFields['config']['CONFIG_FILE']); | ||
49 | } | ||
50 | } | ||
51 | |||
52 | /** | ||
53 | * Test writeConfig function, valid use case, while being logged in. | ||
54 | */ | ||
55 | public function testWriteConfig() | ||
56 | { | ||
57 | writeConfig(self::$_configFields, true); | ||
58 | |||
59 | include self::$_configFields['config']['CONFIG_FILE']; | ||
60 | $this->assertEquals(self::$_configFields['login'], $GLOBALS['login']); | ||
61 | $this->assertEquals(self::$_configFields['hash'], $GLOBALS['hash']); | ||
62 | $this->assertEquals(self::$_configFields['salt'], $GLOBALS['salt']); | ||
63 | $this->assertEquals(self::$_configFields['timezone'], $GLOBALS['timezone']); | ||
64 | $this->assertEquals(self::$_configFields['title'], $GLOBALS['title']); | ||
65 | $this->assertEquals(self::$_configFields['titleLink'], $GLOBALS['titleLink']); | ||
66 | $this->assertEquals(self::$_configFields['redirector'], $GLOBALS['redirector']); | ||
67 | $this->assertEquals(self::$_configFields['disablesessionprotection'], $GLOBALS['disablesessionprotection']); | ||
68 | $this->assertEquals(self::$_configFields['privateLinkByDefault'], $GLOBALS['privateLinkByDefault']); | ||
69 | $this->assertEquals(self::$_configFields['config']['config1'], $GLOBALS['config']['config1']); | ||
70 | $this->assertEquals(self::$_configFields['config']['config2'], $GLOBALS['config']['config2']); | ||
71 | } | ||
72 | |||
73 | /** | ||
74 | * Test writeConfig option while logged in: | ||
75 | * 1. init fields. | ||
76 | * 2. update fields, add new sub config, add new root config. | ||
77 | * 3. rewrite config. | ||
78 | * 4. check result. | ||
79 | */ | ||
80 | public function testWriteConfigFieldUpdate() | ||
81 | { | ||
82 | writeConfig(self::$_configFields, true); | ||
83 | self::$_configFields['title'] = 'ok'; | ||
84 | self::$_configFields['config']['config1'] = 'ok'; | ||
85 | self::$_configFields['config']['config_new'] = 'ok'; | ||
86 | self::$_configFields['new'] = 'should not be saved'; | ||
87 | writeConfig(self::$_configFields, true); | ||
88 | |||
89 | include self::$_configFields['config']['CONFIG_FILE']; | ||
90 | $this->assertEquals('ok', $GLOBALS['title']); | ||
91 | $this->assertEquals('ok', $GLOBALS['config']['config1']); | ||
92 | $this->assertEquals('ok', $GLOBALS['config']['config_new']); | ||
93 | $this->assertFalse(isset($GLOBALS['new'])); | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Test writeConfig function with an empty array. | ||
98 | * | ||
99 | * @expectedException MissingFieldConfigException | ||
100 | */ | ||
101 | public function testWriteConfigEmpty() | ||
102 | { | ||
103 | writeConfig(array(), true); | ||
104 | } | ||
105 | |||
106 | /** | ||
107 | * Test writeConfig function with a missing mandatory field. | ||
108 | * | ||
109 | * @expectedException MissingFieldConfigException | ||
110 | */ | ||
111 | public function testWriteConfigMissingField() | ||
112 | { | ||
113 | unset(self::$_configFields['login']); | ||
114 | writeConfig(self::$_configFields, true); | ||
115 | } | ||
116 | |||
117 | /** | ||
118 | * Test writeConfig function while being logged out, and there is no config file existing. | ||
119 | */ | ||
120 | public function testWriteConfigLoggedOutNoFile() | ||
121 | { | ||
122 | writeConfig(self::$_configFields, false); | ||
123 | } | ||
124 | |||
125 | /** | ||
126 | * Test writeConfig function while being logged out, and a config file already exists. | ||
127 | * | ||
128 | * @expectedException UnauthorizedConfigException | ||
129 | */ | ||
130 | public function testWriteConfigLoggedOutWithFile() | ||
131 | { | ||
132 | file_put_contents(self::$_configFields['config']['CONFIG_FILE'], ''); | ||
133 | writeConfig(self::$_configFields, false); | ||
134 | } | ||
135 | |||
136 | /** | ||
137 | * Test mergeDeprecatedConfig while being logged in: | ||
138 | * 1. init a config file. | ||
139 | * 2. init a options.php file with update value. | ||
140 | * 3. merge. | ||
141 | * 4. check updated value in config file. | ||
142 | */ | ||
143 | public function testMergeDeprecatedConfig() | ||
144 | { | ||
145 | // init | ||
146 | writeConfig(self::$_configFields, true); | ||
147 | $configCopy = self::$_configFields; | ||
148 | $invert = !$configCopy['privateLinkByDefault']; | ||
149 | $configCopy['privateLinkByDefault'] = $invert; | ||
150 | |||
151 | // Use writeConfig to create a options.php | ||
152 | $configCopy['config']['CONFIG_FILE'] = 'tests/options.php'; | ||
153 | writeConfig($configCopy, true); | ||
154 | |||
155 | $this->assertTrue(is_file($configCopy['config']['CONFIG_FILE'])); | ||
156 | |||
157 | // merge configs | ||
158 | mergeDeprecatedConfig(self::$_configFields, true); | ||
159 | |||
160 | // make sure updated field is changed | ||
161 | include self::$_configFields['config']['CONFIG_FILE']; | ||
162 | $this->assertEquals($invert, $GLOBALS['privateLinkByDefault']); | ||
163 | $this->assertFalse(is_file($configCopy['config']['CONFIG_FILE'])); | ||
164 | } | ||
165 | |||
166 | /** | ||
167 | * Test mergeDeprecatedConfig while being logged in without options file. | ||
168 | */ | ||
169 | public function testMergeDeprecatedConfigNoFile() | ||
170 | { | ||
171 | writeConfig(self::$_configFields, true); | ||
172 | mergeDeprecatedConfig(self::$_configFields, true); | ||
173 | |||
174 | include self::$_configFields['config']['CONFIG_FILE']; | ||
175 | $this->assertEquals(self::$_configFields['login'], $GLOBALS['login']); | ||
176 | } | ||
177 | } \ No newline at end of file | ||
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 8b0bd23b..d34ea4f5 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php | |||
@@ -103,7 +103,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
103 | unlink(self::$testDatastore); | 103 | unlink(self::$testDatastore); |
104 | $this->assertFileNotExists(self::$testDatastore); | 104 | $this->assertFileNotExists(self::$testDatastore); |
105 | 105 | ||
106 | $checkDB = self::getMethod('checkDB'); | 106 | $checkDB = self::getMethod('_checkDB'); |
107 | $checkDB->invokeArgs($linkDB, array()); | 107 | $checkDB->invokeArgs($linkDB, array()); |
108 | $this->assertFileExists(self::$testDatastore); | 108 | $this->assertFileExists(self::$testDatastore); |
109 | 109 | ||
@@ -120,7 +120,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
120 | $datastoreSize = filesize(self::$testDatastore); | 120 | $datastoreSize = filesize(self::$testDatastore); |
121 | $this->assertGreaterThan(0, $datastoreSize); | 121 | $this->assertGreaterThan(0, $datastoreSize); |
122 | 122 | ||
123 | $checkDB = self::getMethod('checkDB'); | 123 | $checkDB = self::getMethod('_checkDB'); |
124 | $checkDB->invokeArgs($linkDB, array()); | 124 | $checkDB->invokeArgs($linkDB, array()); |
125 | 125 | ||
126 | // ensure the datastore is left unmodified | 126 | // ensure the datastore is left unmodified |
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index 59ba671f..0b225720 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php | |||
@@ -4,9 +4,9 @@ | |||
4 | */ | 4 | */ |
5 | class ReferenceLinkDB | 5 | class ReferenceLinkDB |
6 | { | 6 | { |
7 | private $links = array(); | 7 | private $_links = array(); |
8 | private $publicCount = 0; | 8 | private $_publicCount = 0; |
9 | private $privateCount = 0; | 9 | private $_privateCount = 0; |
10 | 10 | ||
11 | /** | 11 | /** |
12 | * Populates the test DB with reference data | 12 | * Populates the test DB with reference data |
@@ -81,13 +81,13 @@ class ReferenceLinkDB | |||
81 | 'linkdate' => $date, | 81 | 'linkdate' => $date, |
82 | 'tags' => $tags, | 82 | 'tags' => $tags, |
83 | ); | 83 | ); |
84 | $this->links[$date] = $link; | 84 | $this->_links[$date] = $link; |
85 | 85 | ||
86 | if ($private) { | 86 | if ($private) { |
87 | $this->privateCount++; | 87 | $this->_privateCount++; |
88 | return; | 88 | return; |
89 | } | 89 | } |
90 | $this->publicCount++; | 90 | $this->_publicCount++; |
91 | } | 91 | } |
92 | 92 | ||
93 | /** | 93 | /** |
@@ -97,7 +97,7 @@ class ReferenceLinkDB | |||
97 | { | 97 | { |
98 | file_put_contents( | 98 | file_put_contents( |
99 | $filename, | 99 | $filename, |
100 | '<?php /* '.base64_encode(gzdeflate(serialize($this->links))).' */ ?>' | 100 | '<?php /* '.base64_encode(gzdeflate(serialize($this->_links))).' */ ?>' |
101 | ); | 101 | ); |
102 | } | 102 | } |
103 | 103 | ||
@@ -106,7 +106,7 @@ class ReferenceLinkDB | |||
106 | */ | 106 | */ |
107 | public function countLinks() | 107 | public function countLinks() |
108 | { | 108 | { |
109 | return $this->publicCount + $this->privateCount; | 109 | return $this->_publicCount + $this->_privateCount; |
110 | } | 110 | } |
111 | 111 | ||
112 | /** | 112 | /** |
@@ -114,7 +114,7 @@ class ReferenceLinkDB | |||
114 | */ | 114 | */ |
115 | public function countPublicLinks() | 115 | public function countPublicLinks() |
116 | { | 116 | { |
117 | return $this->publicCount; | 117 | return $this->_publicCount; |
118 | } | 118 | } |
119 | 119 | ||
120 | /** | 120 | /** |
@@ -122,7 +122,7 @@ class ReferenceLinkDB | |||
122 | */ | 122 | */ |
123 | public function countPrivateLinks() | 123 | public function countPrivateLinks() |
124 | { | 124 | { |
125 | return $this->privateCount; | 125 | return $this->_privateCount; |
126 | } | 126 | } |
127 | } | 127 | } |
128 | ?> | 128 | ?> |
diff --git a/tpl/dailyrss.html b/tpl/dailyrss.html index 1b7ab8e9..d959d6be 100644 --- a/tpl/dailyrss.html +++ b/tpl/dailyrss.html | |||
@@ -1,8 +1,16 @@ | |||
1 | {loop="links"} | 1 | <item> |
2 | <h3><a href="{$value.url}">{$value.title}</a></h3> | 2 | <title>{$title} - {function="strftime('%A %e %B %Y', $daydate)"}</title> |
3 | <small>{if="!$GLOBALS['config']['HIDE_TIMESTAMPS']"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> | 3 | <guid>{$absurl}</guid> |
4 | {$value.url}</small><br> | 4 | <link>{$absurl}</link> |
5 | {if="$value.thumbnail"}{$value.thumbnail}{/if}<br> | 5 | <pubDate>{$rfc822date}</pubDate> |
6 | {if="$value.description"}{$value.formatedDescription}{/if} | 6 | <description><![CDATA[ |
7 | <br><br><hr> | 7 | {loop="links"} |
8 | {/loop} \ No newline at end of file | 8 | <h3><a href="{$value.url}">{$value.title}</a></h3> |
9 | <small>{if="!$GLOBALS['config']['HIDE_TIMESTAMPS']"}{function="strftime('%c', $value.timestamp)"} - {/if}{if="$value.tags"}{$value.tags}{/if}<br> | ||
10 | {$value.url}</small><br> | ||
11 | {if="$value.thumbnail"}{$value.thumbnail}{/if}<br> | ||
12 | {if="$value.description"}{$value.formatedDescription}{/if} | ||
13 | <br><br><hr> | ||
14 | {/loop} | ||
15 | ]]></description> | ||
16 | </item> \ No newline at end of file | ||
diff --git a/tpl/editlink.html b/tpl/editlink.html index a32748ab..3733ca21 100644 --- a/tpl/editlink.html +++ b/tpl/editlink.html | |||
@@ -42,7 +42,7 @@ | |||
42 | {if="($GLOBALS['config']['OPEN_SHAARLI'] || isLoggedIn())"} | 42 | {if="($GLOBALS['config']['OPEN_SHAARLI'] || isLoggedIn())"} |
43 | <script> | 43 | <script> |
44 | $ = Awesomplete.$; | 44 | $ = Awesomplete.$; |
45 | new Awesomplete($('input[data-multiple]'), { | 45 | awesomplete = new Awesomplete($('input[data-multiple]'), { |
46 | filter: function(text, input) { | 46 | filter: function(text, input) { |
47 | return Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); | 47 | return Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]); |
48 | }, | 48 | }, |
@@ -52,6 +52,28 @@ | |||
52 | }, | 52 | }, |
53 | minChars: 1 | 53 | minChars: 1 |
54 | }); | 54 | }); |
55 | |||
56 | /** | ||
57 | * Remove already selected items from autocompletion list. | ||
58 | * HTML list is never updated, so removing a tag will add it back to awesomplete. | ||
59 | * | ||
60 | * FIXME: This a workaround waiting for awesomplete to handle this. | ||
61 | * https://github.com/LeaVerou/awesomplete/issues/16749 | ||
62 | */ | ||
63 | var input = document.querySelector('#lf_tags'); | ||
64 | input.addEventListener('input', function() | ||
65 | { | ||
66 | proposedTags = input.getAttribute('data-list').replace(/,/g, '').split(' '); | ||
67 | reg = /(\w+) /g; | ||
68 | while((match = reg.exec(input.value)) !== null) { | ||
69 | id = proposedTags.indexOf(match[1]); | ||
70 | if(id != -1 ) { | ||
71 | proposedTags.splice(id, 1); | ||
72 | } | ||
73 | } | ||
74 | |||
75 | awesomplete.list = proposedTags; | ||
76 | }); | ||
55 | </script> | 77 | </script> |
56 | {/if} | 78 | {/if} |
57 | </body> | 79 | </body> |