aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/Config.php27
-rw-r--r--application/LinkDB.php17
-rw-r--r--application/LinkFilter.php31
-rw-r--r--application/Updater.php243
-rw-r--r--index.php92
-rw-r--r--plugins/wallabag/wallabag.meta2
-rw-r--r--tests/ConfigTest.php42
-rw-r--r--tests/LinkDBTest.php25
-rw-r--r--tests/LinkFilterTest.php16
-rw-r--r--tests/Updater/DummyUpdater.php68
-rw-r--r--tests/Updater/UpdaterTest.php244
-rw-r--r--tests/utils/ReferenceLinkDB.php4
12 files changed, 696 insertions, 115 deletions
diff --git a/application/Config.php b/application/Config.php
index 9af5a535..05a59452 100644
--- a/application/Config.php
+++ b/application/Config.php
@@ -174,33 +174,6 @@ function load_plugin_parameter_values($plugins, $config)
174} 174}
175 175
176/** 176/**
177 * Milestone 0.9 - shaarli/Shaarli#41: options.php is not supported anymore.
178 * ==> if user is loggedIn, merge its content with config.php, then delete options.php.
179 *
180 * @param array $config contains all configuration fields.
181 * @param bool $isLoggedIn true if user is logged in.
182 *
183 * @return void
184 */
185function mergeDeprecatedConfig($config, $isLoggedIn)
186{
187 $config_file = $config['config']['CONFIG_FILE'];
188
189 if (is_file($config['config']['DATADIR'].'/options.php') && $isLoggedIn) {
190 include $config['config']['DATADIR'].'/options.php';
191
192 // Load GLOBALS into config
193 foreach ($GLOBALS as $key => $value) {
194 $config[$key] = $value;
195 }
196 $config['config']['CONFIG_FILE'] = $config_file;
197 writeConfig($config, $isLoggedIn);
198
199 unlink($config['config']['DATADIR'].'/options.php');
200 }
201}
202
203/**
204 * Exception used if a mandatory field is missing in given configuration. 177 * Exception used if a mandatory field is missing in given configuration.
205 */ 178 */
206class MissingFieldConfigException extends Exception 179class MissingFieldConfigException extends Exception
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 19ca6435..9f4d3e3c 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -260,15 +260,19 @@ You use the community supported version of the original Shaarli project, by Seba
260 } 260 }
261 } 261 }
262 262
263 // Keep the list of the mapping URLs-->linkdate up-to-date.
264 $this->_urls = array(); 263 $this->_urls = array();
265 foreach ($this->_links as $link) { 264 foreach ($this->_links as &$link) {
265 // Keep the list of the mapping URLs-->linkdate up-to-date.
266 $this->_urls[$link['url']] = $link['linkdate']; 266 $this->_urls[$link['url']] = $link['linkdate'];
267 }
268 267
269 // Escape links data 268 // Sanitize data fields.
270 foreach($this->_links as &$link) {
271 sanitizeLink($link); 269 sanitizeLink($link);
270
271 // Remove private tags if the user is not logged in.
272 if (! $this->_loggedIn) {
273 $link['tags'] = preg_replace('/(^| )\.[^($| )]+/', '', $link['tags']);
274 }
275
272 // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). 276 // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
273 if (!empty($this->_redirector) && !startsWith($link['url'], '?')) { 277 if (!empty($this->_redirector) && !startsWith($link['url'], '?')) {
274 $link['real_url'] = $this->_redirector . urlencode($link['url']); 278 $link['real_url'] = $this->_redirector . urlencode($link['url']);
@@ -343,7 +347,7 @@ You use the community supported version of the original Shaarli project, by Seba
343 * 347 *
344 * @return array filtered links 348 * @return array filtered links
345 */ 349 */
346 public function filter($type, $request, $casesensitive = false, $privateonly = false) 350 public function filter($type = '', $request = '', $casesensitive = false, $privateonly = false)
347 { 351 {
348 $linkFilter = new LinkFilter($this->_links); 352 $linkFilter = new LinkFilter($this->_links);
349 $requestFilter = is_array($request) ? implode(' ', $request) : $request; 353 $requestFilter = is_array($request) ? implode(' ', $request) : $request;
@@ -381,6 +385,7 @@ You use the community supported version of the original Shaarli project, by Seba
381 } 385 }
382 $linkDays = array_keys($linkDays); 386 $linkDays = array_keys($linkDays);
383 sort($linkDays); 387 sort($linkDays);
388
384 return $linkDays; 389 return $linkDays;
385 } 390 }
386} 391}
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index b2e6530f..ceb47d16 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -209,19 +209,33 @@ class LinkFilter
209 */ 209 */
210 public function filterTags($tags, $casesensitive = false, $privateonly = false) 210 public function filterTags($tags, $casesensitive = false, $privateonly = false)
211 { 211 {
212 $searchtags = $this->tagsStrToArray($tags, $casesensitive); 212 $searchtags = self::tagsStrToArray($tags, $casesensitive);
213 $filtered = array(); 213 $filtered = array();
214 if (empty($searchtags)) {
215 return $filtered;
216 }
214 217
215 foreach ($this->links as $l) { 218 foreach ($this->links as $link) {
216 // ignore non private links when 'privatonly' is on. 219 // ignore non private links when 'privatonly' is on.
217 if (! $l['private'] && $privateonly === true) { 220 if (! $link['private'] && $privateonly === true) {
218 continue; 221 continue;
219 } 222 }
220 223
221 $linktags = $this->tagsStrToArray($l['tags'], $casesensitive); 224 $linktags = self::tagsStrToArray($link['tags'], $casesensitive);
222 225
223 if (count(array_intersect($linktags, $searchtags)) == count($searchtags)) { 226 $found = true;
224 $filtered[$l['linkdate']] = $l; 227 for ($i = 0 ; $i < count($searchtags) && $found; $i++) {
228 // Exclusive search, quit if tag found.
229 // Or, tag not found in the link, quit.
230 if (($searchtags[$i][0] == '-' && in_array(substr($searchtags[$i], 1), $linktags))
231 || ($searchtags[$i][0] != '-') && ! in_array($searchtags[$i], $linktags)
232 ) {
233 $found = false;
234 }
235 }
236
237 if ($found) {
238 $filtered[$link['linkdate']] = $link;
225 } 239 }
226 } 240 }
227 krsort($filtered); 241 krsort($filtered);
@@ -260,19 +274,18 @@ class LinkFilter
260 * Convert a list of tags (str) to an array. Also 274 * Convert a list of tags (str) to an array. Also
261 * - handle case sensitivity. 275 * - handle case sensitivity.
262 * - accepts spaces commas as separator. 276 * - accepts spaces commas as separator.
263 * - remove private tags for loggedout users.
264 * 277 *
265 * @param string $tags string containing a list of tags. 278 * @param string $tags string containing a list of tags.
266 * @param bool $casesensitive will convert everything to lowercase if false. 279 * @param bool $casesensitive will convert everything to lowercase if false.
267 * 280 *
268 * @return array filtered tags string. 281 * @return array filtered tags string.
269 */ 282 */
270 public function tagsStrToArray($tags, $casesensitive) 283 public static function tagsStrToArray($tags, $casesensitive)
271 { 284 {
272 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek) 285 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
273 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8'); 286 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
274 $tagsOut = str_replace(',', ' ', $tagsOut); 287 $tagsOut = str_replace(',', ' ', $tagsOut);
275 288
276 return explode(' ', trim($tagsOut)); 289 return array_filter(explode(' ', trim($tagsOut)), 'strlen');
277 } 290 }
278} 291}
diff --git a/application/Updater.php b/application/Updater.php
new file mode 100644
index 00000000..773a1ffa
--- /dev/null
+++ b/application/Updater.php
@@ -0,0 +1,243 @@
1<?php
2
3/**
4 * Class Updater.
5 * Used to update stuff when a new Shaarli's version is reached.
6 * Update methods are ran only once, and the stored in a JSON file.
7 */
8class Updater
9{
10 /**
11 * @var array Updates which are already done.
12 */
13 protected $doneUpdates;
14
15 /**
16 * @var array Shaarli's configuration array.
17 */
18 protected $config;
19
20 /**
21 * @var LinkDB instance.
22 */
23 protected $linkDB;
24
25 /**
26 * @var bool True if the user is logged in, false otherwise.
27 */
28 protected $isLoggedIn;
29
30 /**
31 * @var ReflectionMethod[] List of current class methods.
32 */
33 protected $methods;
34
35 /**
36 * Object constructor.
37 *
38 * @param array $doneUpdates Updates which are already done.
39 * @param array $config Shaarli's configuration array.
40 * @param LinkDB $linkDB LinkDB instance.
41 * @param boolean $isLoggedIn True if the user is logged in.
42 */
43 public function __construct($doneUpdates, $config, $linkDB, $isLoggedIn)
44 {
45 $this->doneUpdates = $doneUpdates;
46 $this->config = $config;
47 $this->linkDB = $linkDB;
48 $this->isLoggedIn = $isLoggedIn;
49
50 // Retrieve all update methods.
51 $class = new ReflectionClass($this);
52 $this->methods = $class->getMethods();
53 }
54
55 /**
56 * Run all new updates.
57 * Update methods have to start with 'updateMethod' and return true (on success).
58 *
59 * @return array An array containing ran updates.
60 *
61 * @throws UpdaterException If something went wrong.
62 */
63 public function update()
64 {
65 $updatesRan = array();
66
67 // If the user isn't logged in, exit without updating.
68 if ($this->isLoggedIn !== true) {
69 return $updatesRan;
70 }
71
72 if ($this->methods == null) {
73 throw new UpdaterException('Couldn\'t retrieve Updater class methods.');
74 }
75
76 foreach ($this->methods as $method) {
77 // Not an update method or already done, pass.
78 if (! startsWith($method->getName(), 'updateMethod')
79 || in_array($method->getName(), $this->doneUpdates)
80 ) {
81 continue;
82 }
83
84 try {
85 $method->setAccessible(true);
86 $res = $method->invoke($this);
87 // Update method must return true to be considered processed.
88 if ($res === true) {
89 $updatesRan[] = $method->getName();
90 }
91 } catch (Exception $e) {
92 throw new UpdaterException($method, $e);
93 }
94 }
95
96 $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
97
98 return $updatesRan;
99 }
100
101 /**
102 * @return array Updates methods already processed.
103 */
104 public function getDoneUpdates()
105 {
106 return $this->doneUpdates;
107 }
108
109 /**
110 * Move deprecated options.php to config.php.
111 *
112 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
113 * options.php is not supported anymore.
114 */
115 public function updateMethodMergeDeprecatedConfigFile()
116 {
117 $config_file = $this->config['config']['CONFIG_FILE'];
118
119 if (is_file($this->config['config']['DATADIR'].'/options.php')) {
120 include $this->config['config']['DATADIR'].'/options.php';
121
122 // Load GLOBALS into config
123 foreach ($GLOBALS as $key => $value) {
124 $this->config[$key] = $value;
125 }
126 $this->config['config']['CONFIG_FILE'] = $config_file;
127 writeConfig($this->config, $this->isLoggedIn);
128
129 unlink($this->config['config']['DATADIR'].'/options.php');
130 }
131
132 return true;
133 }
134
135 /**
136 * Rename tags starting with a '-' to work with tag exclusion search.
137 */
138 public function updateMethodRenameDashTags()
139 {
140 $linklist = $this->linkDB->filter();
141 foreach ($linklist as $link) {
142 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
144 $this->linkDB[$link['linkdate']] = $link;
145 }
146 $this->linkDB->savedb($this->config['config']['PAGECACHE']);
147 return true;
148 }
149}
150
151/**
152 * Class UpdaterException.
153 */
154class UpdaterException extends Exception
155{
156 /**
157 * @var string Method where the error occurred.
158 */
159 protected $method;
160
161 /**
162 * @var Exception The parent exception.
163 */
164 protected $previous;
165
166 /**
167 * Constructor.
168 *
169 * @param string $message Force the error message if set.
170 * @param string $method Method where the error occurred.
171 * @param Exception|bool $previous Parent exception.
172 */
173 public function __construct($message = '', $method = '', $previous = false)
174 {
175 $this->method = $method;
176 $this->previous = $previous;
177 $this->message = $this->buildMessage($message);
178 }
179
180 /**
181 * Build the exception error message.
182 *
183 * @param string $message Optional given error message.
184 *
185 * @return string The built error message.
186 */
187 private function buildMessage($message)
188 {
189 $out = '';
190 if (! empty($message)) {
191 $out .= $message . PHP_EOL;
192 }
193
194 if (! empty($this->method)) {
195 $out .= 'An error occurred while running the update '. $this->method . PHP_EOL;
196 }
197
198 if (! empty($this->previous)) {
199 $out .= ' '. $this->previous->getMessage();
200 }
201
202 return $out;
203 }
204}
205
206
207/**
208 * Read the updates file, and return already done updates.
209 *
210 * @param string $updatesFilepath Updates file path.
211 *
212 * @return array Already done update methods.
213 */
214function read_updates_file($updatesFilepath)
215{
216 if (! empty($updatesFilepath) && is_file($updatesFilepath)) {
217 $content = file_get_contents($updatesFilepath);
218 if (! empty($content)) {
219 return explode(';', $content);
220 }
221 }
222 return array();
223}
224
225/**
226 * Write updates file.
227 *
228 * @param string $updatesFilepath Updates file path.
229 * @param array $updates Updates array to write.
230 *
231 * @throws Exception Couldn't write version number.
232 */
233function write_updates_file($updatesFilepath, $updates)
234{
235 if (empty($updatesFilepath)) {
236 throw new Exception('Updates file path is not set, can\'t write updates.');
237 }
238
239 $res = file_put_contents($updatesFilepath, implode(';', $updates));
240 if ($res === false) {
241 throw new Exception('Unable to write updates in '. $updatesFilepath . '.');
242 }
243}
diff --git a/index.php b/index.php
index 93e4cc63..3a63319c 100644
--- a/index.php
+++ b/index.php
@@ -44,12 +44,18 @@ $GLOBALS['config']['DATASTORE'] = $GLOBALS['config']['DATADIR'].'/datastore.php'
44// Banned IPs 44// Banned IPs
45$GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php'; 45$GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php';
46 46
47// Processed updates file.
48$GLOBALS['config']['UPDATES_FILE'] = $GLOBALS['config']['DATADIR'].'/updates.txt';
49
47// Access log 50// Access log
48$GLOBALS['config']['LOG_FILE'] = $GLOBALS['config']['DATADIR'].'/log.txt'; 51$GLOBALS['config']['LOG_FILE'] = $GLOBALS['config']['DATADIR'].'/log.txt';
49 52
50// For updates check of Shaarli 53// For updates check of Shaarli
51$GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt'; 54$GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt';
52 55
56// Set ENABLE_UPDATECHECK to disabled by default.
57$GLOBALS['config']['ENABLE_UPDATECHECK'] = false;
58
53// RainTPL cache directory (keep the trailing slash!) 59// RainTPL cache directory (keep the trailing slash!)
54$GLOBALS['config']['RAINTPL_TMP'] = 'tmp/'; 60$GLOBALS['config']['RAINTPL_TMP'] = 'tmp/';
55// Raintpl template directory (keep the trailing slash!) 61// Raintpl template directory (keep the trailing slash!)
@@ -61,7 +67,6 @@ $GLOBALS['config']['CACHEDIR'] = 'cache';
61// Atom & RSS feed cache directory 67// Atom & RSS feed cache directory
62$GLOBALS['config']['PAGECACHE'] = 'pagecache'; 68$GLOBALS['config']['PAGECACHE'] = 'pagecache';
63 69
64
65/* 70/*
66 * Global configuration 71 * Global configuration
67 */ 72 */
@@ -111,7 +116,8 @@ $GLOBALS['config']['UPDATECHECK_INTERVAL'] = 86400;
111//); 116//);
112$GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode'); 117$GLOBALS['config']['ENABLED_PLUGINS'] = array('qrcode');
113 118
114//$GLOBALS['plugins']['WALLABAG_URL'] = 'https://demo.wallabag.org/'; 119// Initialize plugin parameters array.
120$GLOBALS['plugins'] = array();
115 121
116// PubSubHubbub support. Put an empty string to disable, or put your hub url here to enable. 122// PubSubHubbub support. Put an empty string to disable, or put your hub url here to enable.
117$GLOBALS['config']['PUBSUBHUB_URL'] = ''; 123$GLOBALS['config']['PUBSUBHUB_URL'] = '';
@@ -159,6 +165,7 @@ require_once 'application/Utils.php';
159require_once 'application/Config.php'; 165require_once 'application/Config.php';
160require_once 'application/PluginManager.php'; 166require_once 'application/PluginManager.php';
161require_once 'application/Router.php'; 167require_once 'application/Router.php';
168require_once 'application/Updater.php';
162 169
163// Ensure the PHP version is supported 170// Ensure the PHP version is supported
164try { 171try {
@@ -1110,6 +1117,25 @@ function renderPage()
1110 $GLOBALS['redirector'] 1117 $GLOBALS['redirector']
1111 ); 1118 );
1112 1119
1120 $updater = new Updater(
1121 read_updates_file($GLOBALS['config']['UPDATES_FILE']),
1122 $GLOBALS,
1123 $LINKSDB,
1124 isLoggedIn()
1125 );
1126 try {
1127 $newUpdates = $updater->update();
1128 if (! empty($newUpdates)) {
1129 write_updates_file(
1130 $GLOBALS['config']['UPDATES_FILE'],
1131 $updater->getDoneUpdates()
1132 );
1133 }
1134 }
1135 catch(Exception $e) {
1136 die($e->getMessage());
1137 }
1138
1113 $PAGE = new pageBuilder; 1139 $PAGE = new pageBuilder;
1114 1140
1115 // Determine which page will be rendered. 1141 // Determine which page will be rendered.
@@ -1119,9 +1145,9 @@ function renderPage()
1119 // Call plugin hooks for header, footer and includes, specifying which page will be rendered. 1145 // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
1120 // Then assign generated data to RainTPL. 1146 // Then assign generated data to RainTPL.
1121 $common_hooks = array( 1147 $common_hooks = array(
1148 'includes',
1122 'header', 1149 'header',
1123 'footer', 1150 'footer',
1124 'includes',
1125 ); 1151 );
1126 $pluginManager = PluginManager::getInstance(); 1152 $pluginManager = PluginManager::getInstance();
1127 foreach($common_hooks as $name) { 1153 foreach($common_hooks as $name) {
@@ -1540,21 +1566,42 @@ function renderPage()
1540 // -------- User clicked the "Save" button when editing a link: Save link to database. 1566 // -------- User clicked the "Save" button when editing a link: Save link to database.
1541 if (isset($_POST['save_edit'])) 1567 if (isset($_POST['save_edit']))
1542 { 1568 {
1543 if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! 1569 // Go away!
1544 $tags = trim(preg_replace('/\s\s+/',' ', $_POST['lf_tags'])); // Remove multiple spaces. 1570 if (! tokenOk($_POST['token'])) {
1545 $tags = implode(' ', array_unique(explode(' ', $tags))); // Remove duplicates. 1571 die('Wrong token.');
1546 $linkdate=$_POST['lf_linkdate']; 1572 }
1573 // Remove multiple spaces.
1574 $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
1575 // Remove first '-' char in tags.
1576 $tags = preg_replace('/(^| )\-/', '$1', $tags);
1577 // Remove duplicates.
1578 $tags = implode(' ', array_unique(explode(' ', $tags)));
1579 $linkdate = $_POST['lf_linkdate'];
1547 $url = trim($_POST['lf_url']); 1580 $url = trim($_POST['lf_url']);
1548 if (!startsWith($url,'http:') && !startsWith($url,'https:') && !startsWith($url,'ftp:') && !startsWith($url,'magnet:') && !startsWith($url,'?') && !startsWith($url,'javascript:')) 1581 if (! startsWith($url, 'http:') && ! startsWith($url, 'https:')
1549 $url = 'http://'.$url; 1582 && ! startsWith($url, 'ftp:') && ! startsWith($url, 'magnet:')
1550 $link = array('title'=>trim($_POST['lf_title']),'url'=>$url,'description'=>trim($_POST['lf_description']),'private'=>(isset($_POST['lf_private']) ? 1 : 0), 1583 && ! startsWith($url, '?') && ! startsWith($url, 'javascript:')
1551 'linkdate'=>$linkdate,'tags'=>str_replace(',',' ',$tags)); 1584 ) {
1552 if ($link['title']=='') $link['title']=$link['url']; // If title is empty, use the URL as title. 1585 $url = 'http://' . $url;
1586 }
1587
1588 $link = array(
1589 'title' => trim($_POST['lf_title']),
1590 'url' => $url,
1591 'description' => trim($_POST['lf_description']),
1592 'private' => (isset($_POST['lf_private']) ? 1 : 0),
1593 'linkdate' => $linkdate,
1594 'tags' => str_replace(',', ' ', $tags)
1595 );
1596 // If title is empty, use the URL as title.
1597 if ($link['title'] == '') {
1598 $link['title'] = $link['url'];
1599 }
1553 1600
1554 $pluginManager->executeHooks('save_link', $link); 1601 $pluginManager->executeHooks('save_link', $link);
1555 1602
1556 $LINKSDB[$linkdate] = $link; 1603 $LINKSDB[$linkdate] = $link;
1557 $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk. 1604 $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']);
1558 pubsubhub(); 1605 pubsubhub();
1559 1606
1560 // If we are called from the bookmarklet, we must close the popup: 1607 // If we are called from the bookmarklet, we must close the popup:
@@ -1563,10 +1610,12 @@ function renderPage()
1563 exit; 1610 exit;
1564 } 1611 }
1565 1612
1566 $returnurl = !empty($_POST['returnurl']) ? escape($_POST['returnurl']): '?'; 1613 $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
1567 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); 1614 $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
1568 $location .= '#'.smallHash($_POST['lf_linkdate']); // Scroll to the link which has been edited. 1615 // Scroll to the link which has been edited.
1569 header('Location: '. $location); // After saving the link, redirect to the page the user was on. 1616 $location .= '#' . smallHash($_POST['lf_linkdate']);
1617 // After saving the link, redirect to the page the user was on.
1618 header('Location: '. $location);
1570 exit; 1619 exit;
1571 } 1620 }
1572 1621
@@ -1825,7 +1874,7 @@ HTML;
1825 ); 1874 );
1826 1875
1827 // TODO: do not handle exceptions/errors in JS. 1876 // TODO: do not handle exceptions/errors in JS.
1828 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=pluginsadmin\';</script>'; 1877 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do='. Router::$PAGE_PLUGINSADMIN .'\';</script>';
1829 exit; 1878 exit;
1830 } 1879 }
1831 header('Location: ?do='. Router::$PAGE_PLUGINSADMIN); 1880 header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
@@ -2529,15 +2578,6 @@ function resizeImage($filepath)
2529 return true; 2578 return true;
2530} 2579}
2531 2580
2532try {
2533 mergeDeprecatedConfig($GLOBALS, isLoggedIn());
2534} catch(Exception $e) {
2535 error_log(
2536 'ERROR while merging deprecated options.php file.' . PHP_EOL .
2537 $e->getMessage()
2538 );
2539}
2540
2541if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=genthumbnail')) { genThumbnail(); exit; } // Thumbnail generation/cache does not need the link database. 2581if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=genthumbnail')) { genThumbnail(); exit; } // Thumbnail generation/cache does not need the link database.
2542if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; } 2582if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; }
2543if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=atom')) { showATOM(); exit; } 2583if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=atom')) { showATOM(); exit; }
diff --git a/plugins/wallabag/wallabag.meta b/plugins/wallabag/wallabag.meta
index 8763c4a2..26e1ea63 100644
--- a/plugins/wallabag/wallabag.meta
+++ b/plugins/wallabag/wallabag.meta
@@ -1,2 +1,2 @@
1description="For each link, add a Wallabag icon to save it in your instance." 1description="For each link, add a Wallabag icon to save it in your instance."
2parameters="WALLABAG_URL" \ No newline at end of file 2parameters="WALLABAG_URL;WALLABAG_VERSION" \ No newline at end of file
diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php
index 492ddd3b..7200aae6 100644
--- a/tests/ConfigTest.php
+++ b/tests/ConfigTest.php
@@ -134,48 +134,6 @@ class ConfigTest extends PHPUnit_Framework_TestCase
134 } 134 }
135 135
136 /** 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
178 /**
179 * Test save_plugin_config with valid data. 137 * Test save_plugin_config with valid data.
180 * 138 *
181 * @throws PluginConfigOrderException 139 * @throws PluginConfigOrderException
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php
index 3b1a2057..765f771e 100644
--- a/tests/LinkDBTest.php
+++ b/tests/LinkDBTest.php
@@ -276,7 +276,8 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
276 'media' => 1, 276 'media' => 1,
277 'software' => 1, 277 'software' => 1,
278 'stallman' => 1, 278 'stallman' => 1,
279 'free' => 1 279 'free' => 1,
280 '-exclude' => 1,
280 ), 281 ),
281 self::$publicLinkDB->allTags() 282 self::$publicLinkDB->allTags()
282 ); 283 );
@@ -295,7 +296,9 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
295 'html' => 1, 296 'html' => 1,
296 'w3c' => 1, 297 'w3c' => 1,
297 'css' => 1, 298 'css' => 1,
298 'Mercurial' => 1 299 'Mercurial' => 1,
300 '-exclude' => 1,
301 '.hidden' => 1,
299 ), 302 ),
300 self::$privateLinkDB->allTags() 303 self::$privateLinkDB->allTags()
301 ); 304 );
@@ -347,4 +350,22 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
347 count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false)) 350 count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false))
348 ); 351 );
349 } 352 }
353
354 /**
355 * Test hidden tags feature:
356 * tags starting with a dot '.' are only visible when logged in.
357 */
358 public function testHiddenTags()
359 {
360 $tags = '.hidden';
361 $this->assertEquals(
362 1,
363 count(self::$privateLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false))
364 );
365
366 $this->assertEquals(
367 0,
368 count(self::$publicLinkDB->filter(LinkFilter::$FILTER_TAG, $tags, true, false))
369 );
370 }
350} 371}
diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php
index 5fb2423f..164af0d4 100644
--- a/tests/LinkFilterTest.php
+++ b/tests/LinkFilterTest.php
@@ -254,4 +254,20 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
254 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software')) 254 count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, 'free software'))
255 ); 255 );
256 } 256 }
257
258 /**
259 * Tag search with exclusion.
260 */
261 public function testTagFilterWithExclusion()
262 {
263 $this->assertEquals(
264 1,
265 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, 'gnu -free'))
266 );
267
268 $this->assertEquals(
269 5,
270 count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free'))
271 );
272 }
257} 273}
diff --git a/tests/Updater/DummyUpdater.php b/tests/Updater/DummyUpdater.php
new file mode 100644
index 00000000..e9ef2aaa
--- /dev/null
+++ b/tests/Updater/DummyUpdater.php
@@ -0,0 +1,68 @@
1<?php
2
3require_once 'application/Updater.php';
4
5/**
6 * Class DummyUpdater.
7 * Extends Updater to add update method designed for unit tests.
8 */
9class DummyUpdater extends Updater
10{
11 /**
12 * Object constructor.
13 *
14 * @param array $doneUpdates Updates which are already done.
15 * @param array $config Shaarli's configuration array.
16 * @param LinkDB $linkDB LinkDB instance.
17 * @param boolean $isLoggedIn True if the user is logged in.
18 */
19 public function __construct($doneUpdates, $config, $linkDB, $isLoggedIn)
20 {
21 parent::__construct($doneUpdates, $config, $linkDB, $isLoggedIn);
22
23 // Retrieve all update methods.
24 // For unit test, only retrieve final methods,
25 $class = new ReflectionClass($this);
26 $this->methods = $class->getMethods(ReflectionMethod::IS_FINAL);
27 }
28
29 /**
30 * Update method 1.
31 *
32 * @return bool true.
33 */
34 private final function updateMethodDummy1()
35 {
36 return true;
37 }
38
39 /**
40 * Update method 2.
41 *
42 * @return bool true.
43 */
44 private final function updateMethodDummy2()
45 {
46 return true;
47 }
48
49 /**
50 * Update method 3.
51 *
52 * @return bool true.
53 */
54 private final function updateMethodDummy3()
55 {
56 return true;
57 }
58
59 /**
60 * Update method 4, raise an exception.
61 *
62 * @throws Exception error.
63 */
64 private final function updateMethodException()
65 {
66 throw new Exception('whatever');
67 }
68}
diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php
new file mode 100644
index 00000000..d865066b
--- /dev/null
+++ b/tests/Updater/UpdaterTest.php
@@ -0,0 +1,244 @@
1<?php
2
3require_once 'tests/Updater/DummyUpdater.php';
4
5/**
6 * Class UpdaterTest.
7 * Runs unit tests against the Updater class.
8 */
9class UpdaterTest extends PHPUnit_Framework_TestCase
10{
11 /**
12 * @var array Configuration input set.
13 */
14 private static $configFields;
15
16 /**
17 * @var string Path to test datastore.
18 */
19 protected static $testDatastore = 'sandbox/datastore.php';
20
21 /**
22 * Executed before each test.
23 */
24 public function setUp()
25 {
26 self::$configFields = array(
27 'login' => 'login',
28 'hash' => 'hash',
29 'salt' => 'salt',
30 'timezone' => 'Europe/Paris',
31 'title' => 'title',
32 'titleLink' => 'titleLink',
33 'redirector' => '',
34 'disablesessionprotection' => false,
35 'privateLinkByDefault' => false,
36 'config' => array(
37 'CONFIG_FILE' => 'tests/Updater/config.php',
38 'DATADIR' => 'tests/Updater',
39 'PAGECACHE' => 'sandbox/pagecache',
40 'config1' => 'config1data',
41 'config2' => 'config2data',
42 )
43 );
44 }
45
46 /**
47 * Executed after each test.
48 *
49 * @return void
50 */
51 public function tearDown()
52 {
53 if (is_file(self::$configFields['config']['CONFIG_FILE'])) {
54 unlink(self::$configFields['config']['CONFIG_FILE']);
55 }
56
57 if (is_file(self::$configFields['config']['DATADIR'] . '/options.php')) {
58 unlink(self::$configFields['config']['DATADIR'] . '/options.php');
59 }
60
61 if (is_file(self::$configFields['config']['DATADIR'] . '/updates.json')) {
62 unlink(self::$configFields['config']['DATADIR'] . '/updates.json');
63 }
64 }
65
66 /**
67 * Test read_updates_file with an empty/missing file.
68 */
69 public function testReadEmptyUpdatesFile()
70 {
71 $this->assertEquals(array(), read_updates_file(''));
72 $updatesFile = self::$configFields['config']['DATADIR'] . '/updates.json';
73 touch($updatesFile);
74 $this->assertEquals(array(), read_updates_file($updatesFile));
75 }
76
77 /**
78 * Test read/write updates file.
79 */
80 public function testReadWriteUpdatesFile()
81 {
82 $updatesFile = self::$configFields['config']['DATADIR'] . '/updates.json';
83 $updatesMethods = array('m1', 'm2', 'm3');
84
85 write_updates_file($updatesFile, $updatesMethods);
86 $readMethods = read_updates_file($updatesFile);
87 $this->assertEquals($readMethods, $updatesMethods);
88
89 // Update
90 $updatesMethods[] = 'm4';
91 write_updates_file($updatesFile, $updatesMethods);
92 $readMethods = read_updates_file($updatesFile);
93 $this->assertEquals($readMethods, $updatesMethods);
94 }
95
96 /**
97 * Test errors in write_updates_file(): empty updates file.
98 *
99 * @expectedException Exception
100 * @expectedExceptionMessageRegExp /Updates file path is not set(.*)/
101 */
102 public function testWriteEmptyUpdatesFile()
103 {
104 write_updates_file('', array('test'));
105 }
106
107 /**
108 * Test errors in write_updates_file(): not writable updates file.
109 *
110 * @expectedException Exception
111 * @expectedExceptionMessageRegExp /Unable to write(.*)/
112 */
113 public function testWriteUpdatesFileNotWritable()
114 {
115 $updatesFile = self::$configFields['config']['DATADIR'] . '/updates.json';
116 touch($updatesFile);
117 chmod($updatesFile, 0444);
118 @write_updates_file($updatesFile, array('test'));
119 }
120
121 /**
122 * Test the update() method, with no update to run.
123 * 1. Everything already run.
124 * 2. User is logged out.
125 */
126 public function testNoUpdates()
127 {
128 $updates = array(
129 'updateMethodDummy1',
130 'updateMethodDummy2',
131 'updateMethodDummy3',
132 'updateMethodException',
133 );
134 $updater = new DummyUpdater($updates, array(), array(), true);
135 $this->assertEquals(array(), $updater->update());
136
137 $updater = new DummyUpdater(array(), array(), array(), false);
138 $this->assertEquals(array(), $updater->update());
139 }
140
141 /**
142 * Test the update() method, with all updates to run (except the failing one).
143 */
144 public function testUpdatesFirstTime()
145 {
146 $updates = array('updateMethodException',);
147 $expectedUpdates = array(
148 'updateMethodDummy1',
149 'updateMethodDummy2',
150 'updateMethodDummy3',
151 );
152 $updater = new DummyUpdater($updates, array(), array(), true);
153 $this->assertEquals($expectedUpdates, $updater->update());
154 }
155
156 /**
157 * Test the update() method, only one update to run.
158 */
159 public function testOneUpdate()
160 {
161 $updates = array(
162 'updateMethodDummy1',
163 'updateMethodDummy3',
164 'updateMethodException',
165 );
166 $expectedUpdate = array('updateMethodDummy2');
167
168 $updater = new DummyUpdater($updates, array(), array(), true);
169 $this->assertEquals($expectedUpdate, $updater->update());
170 }
171
172 /**
173 * Test Update failed.
174 *
175 * @expectedException UpdaterException
176 */
177 public function testUpdateFailed()
178 {
179 $updates = array(
180 'updateMethodDummy1',
181 'updateMethodDummy2',
182 'updateMethodDummy3',
183 );
184
185 $updater = new DummyUpdater($updates, array(), array(), true);
186 $updater->update();
187 }
188
189 /**
190 * Test update mergeDeprecatedConfig:
191 * 1. init a config file.
192 * 2. init a options.php file with update value.
193 * 3. merge.
194 * 4. check updated value in config file.
195 */
196 public function testUpdateMergeDeprecatedConfig()
197 {
198 // init
199 writeConfig(self::$configFields, true);
200 $configCopy = self::$configFields;
201 $invert = !$configCopy['privateLinkByDefault'];
202 $configCopy['privateLinkByDefault'] = $invert;
203
204 // Use writeConfig to create a options.php
205 $configCopy['config']['CONFIG_FILE'] = 'tests/Updater/options.php';
206 writeConfig($configCopy, true);
207
208 $this->assertTrue(is_file($configCopy['config']['CONFIG_FILE']));
209
210 // merge configs
211 $updater = new Updater(array(), self::$configFields, array(), true);
212 $updater->updateMethodMergeDeprecatedConfigFile();
213
214 // make sure updated field is changed
215 include self::$configFields['config']['CONFIG_FILE'];
216 $this->assertEquals($invert, $GLOBALS['privateLinkByDefault']);
217 $this->assertFalse(is_file($configCopy['config']['CONFIG_FILE']));
218 }
219
220 /**
221 * Test mergeDeprecatedConfig in without options file.
222 */
223 public function testMergeDeprecatedConfigNoFile()
224 {
225 writeConfig(self::$configFields, true);
226
227 $updater = new Updater(array(), self::$configFields, array(), true);
228 $updater->updateMethodMergeDeprecatedConfigFile();
229
230 include self::$configFields['config']['CONFIG_FILE'];
231 $this->assertEquals(self::$configFields['login'], $GLOBALS['login']);
232 }
233
234 public function testRenameDashTags()
235 {
236 $refDB = new ReferenceLinkDB();
237 $refDB->write(self::$testDatastore);
238 $linkDB = new LinkDB(self::$testDatastore, true, false);
239 $this->assertEmpty($linkDB->filter(LinkFilter::$FILTER_TAG, 'exclude'));
240 $updater = new Updater(array(), self::$configFields, $linkDB, true);
241 $updater->updateMethodRenameDashTags();
242 $this->assertNotEmpty($linkDB->filter(LinkFilter::$FILTER_TAG, 'exclude'));
243 }
244}
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index 011317ef..da3e8c65 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -19,7 +19,7 @@ class ReferenceLinkDB
19 'Richard Stallman and the Free Software Revolution', 19 'Richard Stallman and the Free Software Revolution',
20 0, 20 0,
21 '20150310_114633', 21 '20150310_114633',
22 'free gnu software stallman' 22 'free gnu software stallman -exclude'
23 ); 23 );
24 24
25 $this->addLink( 25 $this->addLink(
@@ -28,7 +28,7 @@ class ReferenceLinkDB
28 'A free software media publishing platform', 28 'A free software media publishing platform',
29 0, 29 0,
30 '20130614_184135', 30 '20130614_184135',
31 'gnu media web' 31 'gnu media web .hidden'
32 ); 32 );
33 33
34 $this->addLink( 34 $this->addLink(