diff options
author | VirtualTam <virtualtam@flibidi.net> | 2015-11-11 22:49:58 +0100 |
---|---|---|
committer | VirtualTam <virtualtam@flibidi.net> | 2015-11-24 01:12:35 +0100 |
commit | 2e28269baed195d58bbe169841eed176b171db76 (patch) | |
tree | f743e785edf708454ab53efa13f38e35f10447e6 | |
parent | c580024cfbe5f0d290b09157b9665d1b4131d7f4 (diff) | |
download | Shaarli-2e28269baed195d58bbe169841eed176b171db76.tar.gz Shaarli-2e28269baed195d58bbe169841eed176b171db76.tar.zst Shaarli-2e28269baed195d58bbe169841eed176b171db76.zip |
install: check file/directory permissions for Shaarli resources
Relates to #40
Relates to #372
Additions:
- FileUtils: IOException
- ApplicationUtils:
- check if Shaarli resources are accessible with sufficient permissions
- basic test coverage
- index.php:
- check access permissions and redirect to an error page if needed:
- before running the first installation
Modifications:
- LinkDB:
- factorize datastore write code
- check if the datastore
(exists AND is writeable) OR (doesn't exist AND its parent dir is writable)
- raise an IOException if needed
Signed-off-by: VirtualTam <virtualtam@flibidi.net>
-rw-r--r-- | application/ApplicationUtils.php | 69 | ||||
-rw-r--r-- | application/FileUtils.php | 19 | ||||
-rw-r--r-- | application/LinkDB.php | 35 | ||||
-rw-r--r-- | index.php | 36 | ||||
-rw-r--r-- | tests/ApplicationUtilsTest.php | 69 | ||||
-rw-r--r-- | tests/LinkDBTest.php | 5 |
6 files changed, 213 insertions, 20 deletions
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php new file mode 100644 index 00000000..6fb07f36 --- /dev/null +++ b/application/ApplicationUtils.php | |||
@@ -0,0 +1,69 @@ | |||
1 | <?php | ||
2 | /** | ||
3 | * Shaarli (application) utilities | ||
4 | */ | ||
5 | class ApplicationUtils | ||
6 | { | ||
7 | |||
8 | /** | ||
9 | * Checks Shaarli has the proper access permissions to its resources | ||
10 | * | ||
11 | * @param array $globalConfig The $GLOBALS['config'] array | ||
12 | * | ||
13 | * @return array A list of the detected configuration issues | ||
14 | */ | ||
15 | public static function checkResourcePermissions($globalConfig) | ||
16 | { | ||
17 | $errors = array(); | ||
18 | |||
19 | // Check script and template directories are readable | ||
20 | foreach (array( | ||
21 | 'application', | ||
22 | 'inc', | ||
23 | 'plugins', | ||
24 | $globalConfig['RAINTPL_TPL'] | ||
25 | ) as $path) { | ||
26 | if (! is_readable(realpath($path))) { | ||
27 | $errors[] = '"'.$path.'" directory is not readable'; | ||
28 | } | ||
29 | } | ||
30 | |||
31 | // Check cache and data directories are readable and writeable | ||
32 | foreach (array( | ||
33 | $globalConfig['CACHEDIR'], | ||
34 | $globalConfig['DATADIR'], | ||
35 | $globalConfig['PAGECACHE'], | ||
36 | $globalConfig['RAINTPL_TMP'] | ||
37 | ) as $path) { | ||
38 | if (! is_readable(realpath($path))) { | ||
39 | $errors[] = '"'.$path.'" directory is not readable'; | ||
40 | } | ||
41 | if (! is_writable(realpath($path))) { | ||
42 | $errors[] = '"'.$path.'" directory is not writable'; | ||
43 | } | ||
44 | } | ||
45 | |||
46 | // Check configuration files are readable and writeable | ||
47 | foreach (array( | ||
48 | $globalConfig['CONFIG_FILE'], | ||
49 | $globalConfig['DATASTORE'], | ||
50 | $globalConfig['IPBANS_FILENAME'], | ||
51 | $globalConfig['LOG_FILE'], | ||
52 | $globalConfig['UPDATECHECK_FILENAME'] | ||
53 | ) as $path) { | ||
54 | if (! is_file(realpath($path))) { | ||
55 | # the file may not exist yet | ||
56 | continue; | ||
57 | } | ||
58 | |||
59 | if (! is_readable(realpath($path))) { | ||
60 | $errors[] = '"'.$path.'" file is not readable'; | ||
61 | } | ||
62 | if (! is_writable(realpath($path))) { | ||
63 | $errors[] = '"'.$path.'" file is not writable'; | ||
64 | } | ||
65 | } | ||
66 | |||
67 | return $errors; | ||
68 | } | ||
69 | } | ||
diff --git a/application/FileUtils.php b/application/FileUtils.php new file mode 100644 index 00000000..6a12ef0e --- /dev/null +++ b/application/FileUtils.php | |||
@@ -0,0 +1,19 @@ | |||
1 | <?php | ||
2 | /** | ||
3 | * Exception class thrown when a filesystem access failure happens | ||
4 | */ | ||
5 | class IOException extends Exception | ||
6 | { | ||
7 | private $path; | ||
8 | |||
9 | /** | ||
10 | * Construct a new IOException | ||
11 | * | ||
12 | * @param string $path path to the ressource that cannot be accessed | ||
13 | */ | ||
14 | public function __construct($path) | ||
15 | { | ||
16 | $this->path = $path; | ||
17 | $this->message = 'Error accessing '.$this->path; | ||
18 | } | ||
19 | } | ||
diff --git a/application/LinkDB.php b/application/LinkDB.php index 84733505..15fadbc3 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -212,11 +212,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
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 | $this->writeDB(); |
216 | file_put_contents( | ||
217 | $this->_datastore, | ||
218 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix | ||
219 | ); | ||
220 | } | 216 | } |
221 | 217 | ||
222 | /** | 218 | /** |
@@ -270,6 +266,28 @@ You use the community supported version of the original Shaarli project, by Seba | |||
270 | /** | 266 | /** |
271 | * Saves the database from memory to disk | 267 | * Saves the database from memory to disk |
272 | * | 268 | * |
269 | * @throws IOException the datastore is not writable | ||
270 | */ | ||
271 | private function writeDB() | ||
272 | { | ||
273 | if (is_file($this->_datastore) && !is_writeable($this->_datastore)) { | ||
274 | // The datastore exists but is not writeable | ||
275 | throw new IOException($this->_datastore); | ||
276 | } else if (!is_file($this->_datastore) && !is_writeable(dirname($this->_datastore))) { | ||
277 | // The datastore does not exist and its parent directory is not writeable | ||
278 | throw new IOException(dirname($this->_datastore)); | ||
279 | } | ||
280 | |||
281 | file_put_contents( | ||
282 | $this->_datastore, | ||
283 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix | ||
284 | ); | ||
285 | |||
286 | } | ||
287 | |||
288 | /** | ||
289 | * Saves the database from memory to disk | ||
290 | * | ||
273 | * @param string $pageCacheDir page cache directory | 291 | * @param string $pageCacheDir page cache directory |
274 | */ | 292 | */ |
275 | public function savedb($pageCacheDir) | 293 | public function savedb($pageCacheDir) |
@@ -278,10 +296,9 @@ You use the community supported version of the original Shaarli project, by Seba | |||
278 | // TODO: raise an Exception instead | 296 | // TODO: raise an Exception instead |
279 | die('You are not authorized to change the database.'); | 297 | die('You are not authorized to change the database.'); |
280 | } | 298 | } |
281 | file_put_contents( | 299 | |
282 | $this->_datastore, | 300 | $this->writeDB(); |
283 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix | 301 | |
284 | ); | ||
285 | invalidateCaches($pageCacheDir); | 302 | invalidateCaches($pageCacheDir); |
286 | } | 303 | } |
287 | 304 | ||
@@ -44,6 +44,9 @@ $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 | // Access log | ||
48 | $GLOBALS['config']['LOG_FILE'] = $GLOBALS['config']['DATADIR'].'/log.txt'; | ||
49 | |||
47 | // For updates check of Shaarli | 50 | // For updates check of Shaarli |
48 | $GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt'; | 51 | $GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt'; |
49 | 52 | ||
@@ -52,7 +55,7 @@ $GLOBALS['config']['RAINTPL_TMP'] = 'tmp/'; | |||
52 | // Raintpl template directory (keep the trailing slash!) | 55 | // Raintpl template directory (keep the trailing slash!) |
53 | $GLOBALS['config']['RAINTPL_TPL'] = 'tpl/'; | 56 | $GLOBALS['config']['RAINTPL_TPL'] = 'tpl/'; |
54 | 57 | ||
55 | // Thuumbnail cache directory | 58 | // Thumbnail cache directory |
56 | $GLOBALS['config']['CACHEDIR'] = 'cache'; | 59 | $GLOBALS['config']['CACHEDIR'] = 'cache'; |
57 | 60 | ||
58 | // Atom & RSS feed cache directory | 61 | // Atom & RSS feed cache directory |
@@ -141,8 +144,10 @@ if (is_file($GLOBALS['config']['CONFIG_FILE'])) { | |||
141 | } | 144 | } |
142 | 145 | ||
143 | // Shaarli library | 146 | // Shaarli library |
147 | require_once 'application/ApplicationUtils.php'; | ||
144 | require_once 'application/Cache.php'; | 148 | require_once 'application/Cache.php'; |
145 | require_once 'application/CachedPage.php'; | 149 | require_once 'application/CachedPage.php'; |
150 | require_once 'application/FileUtils.php'; | ||
146 | require_once 'application/HttpUtils.php'; | 151 | require_once 'application/HttpUtils.php'; |
147 | require_once 'application/LinkDB.php'; | 152 | require_once 'application/LinkDB.php'; |
148 | require_once 'application/TimeZone.php'; | 153 | require_once 'application/TimeZone.php'; |
@@ -155,9 +160,9 @@ require_once 'application/Router.php'; | |||
155 | // Ensure the PHP version is supported | 160 | // Ensure the PHP version is supported |
156 | try { | 161 | try { |
157 | checkPHPVersion('5.3', PHP_VERSION); | 162 | checkPHPVersion('5.3', PHP_VERSION); |
158 | } catch(Exception $e) { | 163 | } catch(Exception $exc) { |
159 | header('Content-Type: text/plain; charset=utf-8'); | 164 | header('Content-Type: text/plain; charset=utf-8'); |
160 | echo $e->getMessage(); | 165 | echo $exc->getMessage(); |
161 | exit; | 166 | exit; |
162 | } | 167 | } |
163 | 168 | ||
@@ -216,9 +221,6 @@ header("Cache-Control: no-store, no-cache, must-revalidate"); | |||
216 | header("Cache-Control: post-check=0, pre-check=0", false); | 221 | header("Cache-Control: post-check=0, pre-check=0", false); |
217 | header("Pragma: no-cache"); | 222 | header("Pragma: no-cache"); |
218 | 223 | ||
219 | // Directories creations (Note that your web host may require different rights than 705.) | ||
220 | if (!is_writable(realpath(dirname(__FILE__)))) die('<pre>ERROR: Shaarli does not have the right to write in its own directory.</pre>'); | ||
221 | |||
222 | // Handling of old config file which do not have the new parameters. | 224 | // Handling of old config file which do not have the new parameters. |
223 | if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.escape(index_url($_SERVER)); | 225 | if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.escape(index_url($_SERVER)); |
224 | if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get(); | 226 | if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get(); |
@@ -228,8 +230,24 @@ if (empty($GLOBALS['privateLinkByDefault'])) $GLOBALS['privateLinkByDefault']=fa | |||
228 | if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?'; | 230 | if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?'; |
229 | // I really need to rewrite Shaarli with a proper configuation manager. | 231 | // I really need to rewrite Shaarli with a proper configuation manager. |
230 | 232 | ||
231 | // Run config screen if first run: | ||
232 | if (! is_file($GLOBALS['config']['CONFIG_FILE'])) { | 233 | if (! is_file($GLOBALS['config']['CONFIG_FILE'])) { |
234 | // Ensure Shaarli has proper access to its resources | ||
235 | $errors = ApplicationUtils::checkResourcePermissions($GLOBALS['config']); | ||
236 | |||
237 | if ($errors != array()) { | ||
238 | $message = '<p>Insufficient permissions:</p><ul>'; | ||
239 | |||
240 | foreach ($errors as $error) { | ||
241 | $message .= '<li>'.$error.'</li>'; | ||
242 | } | ||
243 | $message .= '</ul>'; | ||
244 | |||
245 | header('Content-Type: text/html; charset=utf-8'); | ||
246 | echo $message; | ||
247 | exit; | ||
248 | } | ||
249 | |||
250 | // Display the installation form if no existing config is found | ||
233 | install(); | 251 | install(); |
234 | } | 252 | } |
235 | 253 | ||
@@ -319,7 +337,7 @@ function checkUpdate() | |||
319 | function logm($message) | 337 | function logm($message) |
320 | { | 338 | { |
321 | $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n"; | 339 | $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n"; |
322 | file_put_contents($GLOBALS['config']['DATADIR'].'/log.txt',$t,FILE_APPEND); | 340 | file_put_contents($GLOBAL['config']['LOG_FILE'], $t, FILE_APPEND); |
323 | } | 341 | } |
324 | 342 | ||
325 | // In a string, converts URLs to clickable links. | 343 | // In a string, converts URLs to clickable links. |
@@ -1461,7 +1479,7 @@ function renderPage() | |||
1461 | $value['tags']=trim(implode(' ',$tags)); | 1479 | $value['tags']=trim(implode(' ',$tags)); |
1462 | $LINKSDB[$key]=$value; | 1480 | $LINKSDB[$key]=$value; |
1463 | } | 1481 | } |
1464 | $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk. | 1482 | $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); |
1465 | echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>'; | 1483 | echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>'; |
1466 | exit; | 1484 | exit; |
1467 | } | 1485 | } |
diff --git a/tests/ApplicationUtilsTest.php b/tests/ApplicationUtilsTest.php new file mode 100644 index 00000000..9a99c6c6 --- /dev/null +++ b/tests/ApplicationUtilsTest.php | |||
@@ -0,0 +1,69 @@ | |||
1 | <?php | ||
2 | /** | ||
3 | * ApplicationUtils' tests | ||
4 | */ | ||
5 | |||
6 | require_once 'application/ApplicationUtils.php'; | ||
7 | |||
8 | |||
9 | /** | ||
10 | * Unitary tests for Shaarli utilities | ||
11 | */ | ||
12 | class ApplicationUtilsTest extends PHPUnit_Framework_TestCase | ||
13 | { | ||
14 | /** | ||
15 | * Checks resource permissions for the current Shaarli installation | ||
16 | */ | ||
17 | public function testCheckCurrentResourcePermissions() | ||
18 | { | ||
19 | $config = array( | ||
20 | 'CACHEDIR' => 'cache', | ||
21 | 'CONFIG_FILE' => 'data/config.php', | ||
22 | 'DATADIR' => 'data', | ||
23 | 'DATASTORE' => 'data/datastore.php', | ||
24 | 'IPBANS_FILENAME' => 'data/ipbans.php', | ||
25 | 'LOG_FILE' => 'data/log.txt', | ||
26 | 'PAGECACHE' => 'pagecache', | ||
27 | 'RAINTPL_TMP' => 'tmp', | ||
28 | 'RAINTPL_TPL' => 'tpl', | ||
29 | 'UPDATECHECK_FILENAME' => 'data/lastupdatecheck.txt' | ||
30 | ); | ||
31 | $this->assertEquals( | ||
32 | array(), | ||
33 | ApplicationUtils::checkResourcePermissions($config) | ||
34 | ); | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * Checks resource permissions for a non-existent Shaarli installation | ||
39 | */ | ||
40 | public function testCheckCurrentResourcePermissionsErrors() | ||
41 | { | ||
42 | $config = array( | ||
43 | 'CACHEDIR' => 'null/cache', | ||
44 | 'CONFIG_FILE' => 'null/data/config.php', | ||
45 | 'DATADIR' => 'null/data', | ||
46 | 'DATASTORE' => 'null/data/store.php', | ||
47 | 'IPBANS_FILENAME' => 'null/data/ipbans.php', | ||
48 | 'LOG_FILE' => 'null/data/log.txt', | ||
49 | 'PAGECACHE' => 'null/pagecache', | ||
50 | 'RAINTPL_TMP' => 'null/tmp', | ||
51 | 'RAINTPL_TPL' => 'null/tpl', | ||
52 | 'UPDATECHECK_FILENAME' => 'null/data/lastupdatecheck.txt' | ||
53 | ); | ||
54 | $this->assertEquals( | ||
55 | array( | ||
56 | '"null/tpl" directory is not readable', | ||
57 | '"null/cache" directory is not readable', | ||
58 | '"null/cache" directory is not writable', | ||
59 | '"null/data" directory is not readable', | ||
60 | '"null/data" directory is not writable', | ||
61 | '"null/pagecache" directory is not readable', | ||
62 | '"null/pagecache" directory is not writable', | ||
63 | '"null/tmp" directory is not readable', | ||
64 | '"null/tmp" directory is not writable' | ||
65 | ), | ||
66 | ApplicationUtils::checkResourcePermissions($config) | ||
67 | ); | ||
68 | } | ||
69 | } | ||
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 451f1d6f..8929713d 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php | |||
@@ -4,6 +4,7 @@ | |||
4 | */ | 4 | */ |
5 | 5 | ||
6 | require_once 'application/Cache.php'; | 6 | require_once 'application/Cache.php'; |
7 | require_once 'application/FileUtils.php'; | ||
7 | require_once 'application/LinkDB.php'; | 8 | require_once 'application/LinkDB.php'; |
8 | require_once 'application/Utils.php'; | 9 | require_once 'application/Utils.php'; |
9 | require_once 'tests/utils/ReferenceLinkDB.php'; | 10 | require_once 'tests/utils/ReferenceLinkDB.php'; |
@@ -87,8 +88,8 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
87 | /** | 88 | /** |
88 | * Attempt to instantiate a LinkDB whereas the datastore is not writable | 89 | * Attempt to instantiate a LinkDB whereas the datastore is not writable |
89 | * | 90 | * |
90 | * @expectedException PHPUnit_Framework_Error_Warning | 91 | * @expectedException IOException |
91 | * @expectedExceptionMessageRegExp /failed to open stream: No such file or directory/ | 92 | * @expectedExceptionMessageRegExp /Error accessing null/ |
92 | */ | 93 | */ |
93 | public function testConstructDatastoreNotWriteable() | 94 | public function testConstructDatastoreNotWriteable() |
94 | { | 95 | { |