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>
This commit is contained in:
VirtualTam 2015-11-11 22:49:58 +01:00
parent c580024cfb
commit 2e28269bae
6 changed files with 213 additions and 20 deletions

View file

@ -0,0 +1,69 @@
<?php
/**
* Shaarli (application) utilities
*/
class ApplicationUtils
{
/**
* Checks Shaarli has the proper access permissions to its resources
*
* @param array $globalConfig The $GLOBALS['config'] array
*
* @return array A list of the detected configuration issues
*/
public static function checkResourcePermissions($globalConfig)
{
$errors = array();
// Check script and template directories are readable
foreach (array(
'application',
'inc',
'plugins',
$globalConfig['RAINTPL_TPL']
) as $path) {
if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable';
}
}
// Check cache and data directories are readable and writeable
foreach (array(
$globalConfig['CACHEDIR'],
$globalConfig['DATADIR'],
$globalConfig['PAGECACHE'],
$globalConfig['RAINTPL_TMP']
) as $path) {
if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable';
}
if (! is_writable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not writable';
}
}
// Check configuration files are readable and writeable
foreach (array(
$globalConfig['CONFIG_FILE'],
$globalConfig['DATASTORE'],
$globalConfig['IPBANS_FILENAME'],
$globalConfig['LOG_FILE'],
$globalConfig['UPDATECHECK_FILENAME']
) as $path) {
if (! is_file(realpath($path))) {
# the file may not exist yet
continue;
}
if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" file is not readable';
}
if (! is_writable(realpath($path))) {
$errors[] = '"'.$path.'" file is not writable';
}
}
return $errors;
}
}

19
application/FileUtils.php Normal file
View file

@ -0,0 +1,19 @@
<?php
/**
* Exception class thrown when a filesystem access failure happens
*/
class IOException extends Exception
{
private $path;
/**
* Construct a new IOException
*
* @param string $path path to the ressource that cannot be accessed
*/
public function __construct($path)
{
$this->path = $path;
$this->message = 'Error accessing '.$this->path;
}
}

View file

@ -212,11 +212,7 @@ private function _checkDB()
$this->_links[$link['linkdate']] = $link; $this->_links[$link['linkdate']] = $link;
// Write database to disk // Write database to disk
// TODO: raise an exception if the file is not write-able $this->writeDB();
file_put_contents(
$this->_datastore,
self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix
);
} }
/** /**
@ -267,6 +263,28 @@ private function _readDB()
} }
} }
/**
* Saves the database from memory to disk
*
* @throws IOException the datastore is not writable
*/
private function writeDB()
{
if (is_file($this->_datastore) && !is_writeable($this->_datastore)) {
// The datastore exists but is not writeable
throw new IOException($this->_datastore);
} else if (!is_file($this->_datastore) && !is_writeable(dirname($this->_datastore))) {
// The datastore does not exist and its parent directory is not writeable
throw new IOException(dirname($this->_datastore));
}
file_put_contents(
$this->_datastore,
self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix
);
}
/** /**
* Saves the database from memory to disk * Saves the database from memory to disk
* *
@ -278,10 +296,9 @@ public function savedb($pageCacheDir)
// TODO: raise an Exception instead // TODO: raise an Exception instead
die('You are not authorized to change the database.'); die('You are not authorized to change the database.');
} }
file_put_contents(
$this->_datastore, $this->writeDB();
self::$phpPrefix.base64_encode(gzdeflate(serialize($this->_links))).self::$phpSuffix
);
invalidateCaches($pageCacheDir); invalidateCaches($pageCacheDir);
} }

View file

@ -44,6 +44,9 @@
// Banned IPs // Banned IPs
$GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php'; $GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php';
// Access log
$GLOBALS['config']['LOG_FILE'] = $GLOBALS['config']['DATADIR'].'/log.txt';
// For updates check of Shaarli // For updates check of Shaarli
$GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt'; $GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt';
@ -52,7 +55,7 @@
// Raintpl template directory (keep the trailing slash!) // Raintpl template directory (keep the trailing slash!)
$GLOBALS['config']['RAINTPL_TPL'] = 'tpl/'; $GLOBALS['config']['RAINTPL_TPL'] = 'tpl/';
// Thuumbnail cache directory // Thumbnail cache directory
$GLOBALS['config']['CACHEDIR'] = 'cache'; $GLOBALS['config']['CACHEDIR'] = 'cache';
// Atom & RSS feed cache directory // Atom & RSS feed cache directory
@ -141,8 +144,10 @@
} }
// Shaarli library // Shaarli library
require_once 'application/ApplicationUtils.php';
require_once 'application/Cache.php'; require_once 'application/Cache.php';
require_once 'application/CachedPage.php'; require_once 'application/CachedPage.php';
require_once 'application/FileUtils.php';
require_once 'application/HttpUtils.php'; require_once 'application/HttpUtils.php';
require_once 'application/LinkDB.php'; require_once 'application/LinkDB.php';
require_once 'application/TimeZone.php'; require_once 'application/TimeZone.php';
@ -155,9 +160,9 @@
// Ensure the PHP version is supported // Ensure the PHP version is supported
try { try {
checkPHPVersion('5.3', PHP_VERSION); checkPHPVersion('5.3', PHP_VERSION);
} catch(Exception $e) { } catch(Exception $exc) {
header('Content-Type: text/plain; charset=utf-8'); header('Content-Type: text/plain; charset=utf-8');
echo $e->getMessage(); echo $exc->getMessage();
exit; exit;
} }
@ -216,9 +221,6 @@ function stripslashes_deep($value) { $value = is_array($value) ? array_map('stri
header("Cache-Control: post-check=0, pre-check=0", false); header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache"); header("Pragma: no-cache");
// Directories creations (Note that your web host may require different rights than 705.)
if (!is_writable(realpath(dirname(__FILE__)))) die('<pre>ERROR: Shaarli does not have the right to write in its own directory.</pre>');
// Handling of old config file which do not have the new parameters. // Handling of old config file which do not have the new parameters.
if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.escape(index_url($_SERVER)); if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.escape(index_url($_SERVER));
if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get(); if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get();
@ -228,8 +230,24 @@ function stripslashes_deep($value) { $value = is_array($value) ? array_map('stri
if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?'; if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?';
// I really need to rewrite Shaarli with a proper configuation manager. // I really need to rewrite Shaarli with a proper configuation manager.
// Run config screen if first run:
if (! is_file($GLOBALS['config']['CONFIG_FILE'])) { if (! is_file($GLOBALS['config']['CONFIG_FILE'])) {
// Ensure Shaarli has proper access to its resources
$errors = ApplicationUtils::checkResourcePermissions($GLOBALS['config']);
if ($errors != array()) {
$message = '<p>Insufficient permissions:</p><ul>';
foreach ($errors as $error) {
$message .= '<li>'.$error.'</li>';
}
$message .= '</ul>';
header('Content-Type: text/html; charset=utf-8');
echo $message;
exit;
}
// Display the installation form if no existing config is found
install(); install();
} }
@ -319,7 +337,7 @@ function checkUpdate()
function logm($message) function logm($message)
{ {
$t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n"; $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n";
file_put_contents($GLOBALS['config']['DATADIR'].'/log.txt',$t,FILE_APPEND); file_put_contents($GLOBAL['config']['LOG_FILE'], $t, FILE_APPEND);
} }
// In a string, converts URLs to clickable links. // In a string, converts URLs to clickable links.
@ -1461,7 +1479,7 @@ function renderPage()
$value['tags']=trim(implode(' ',$tags)); $value['tags']=trim(implode(' ',$tags));
$LINKSDB[$key]=$value; $LINKSDB[$key]=$value;
} }
$LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk. $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']);
echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>'; echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>';
exit; exit;
} }

View file

@ -0,0 +1,69 @@
<?php
/**
* ApplicationUtils' tests
*/
require_once 'application/ApplicationUtils.php';
/**
* Unitary tests for Shaarli utilities
*/
class ApplicationUtilsTest extends PHPUnit_Framework_TestCase
{
/**
* Checks resource permissions for the current Shaarli installation
*/
public function testCheckCurrentResourcePermissions()
{
$config = array(
'CACHEDIR' => 'cache',
'CONFIG_FILE' => 'data/config.php',
'DATADIR' => 'data',
'DATASTORE' => 'data/datastore.php',
'IPBANS_FILENAME' => 'data/ipbans.php',
'LOG_FILE' => 'data/log.txt',
'PAGECACHE' => 'pagecache',
'RAINTPL_TMP' => 'tmp',
'RAINTPL_TPL' => 'tpl',
'UPDATECHECK_FILENAME' => 'data/lastupdatecheck.txt'
);
$this->assertEquals(
array(),
ApplicationUtils::checkResourcePermissions($config)
);
}
/**
* Checks resource permissions for a non-existent Shaarli installation
*/
public function testCheckCurrentResourcePermissionsErrors()
{
$config = array(
'CACHEDIR' => 'null/cache',
'CONFIG_FILE' => 'null/data/config.php',
'DATADIR' => 'null/data',
'DATASTORE' => 'null/data/store.php',
'IPBANS_FILENAME' => 'null/data/ipbans.php',
'LOG_FILE' => 'null/data/log.txt',
'PAGECACHE' => 'null/pagecache',
'RAINTPL_TMP' => 'null/tmp',
'RAINTPL_TPL' => 'null/tpl',
'UPDATECHECK_FILENAME' => 'null/data/lastupdatecheck.txt'
);
$this->assertEquals(
array(
'"null/tpl" directory is not readable',
'"null/cache" directory is not readable',
'"null/cache" directory is not writable',
'"null/data" directory is not readable',
'"null/data" directory is not writable',
'"null/pagecache" directory is not readable',
'"null/pagecache" directory is not writable',
'"null/tmp" directory is not readable',
'"null/tmp" directory is not writable'
),
ApplicationUtils::checkResourcePermissions($config)
);
}
}

View file

@ -4,6 +4,7 @@
*/ */
require_once 'application/Cache.php'; require_once 'application/Cache.php';
require_once 'application/FileUtils.php';
require_once 'application/LinkDB.php'; require_once 'application/LinkDB.php';
require_once 'application/Utils.php'; require_once 'application/Utils.php';
require_once 'tests/utils/ReferenceLinkDB.php'; require_once 'tests/utils/ReferenceLinkDB.php';
@ -87,8 +88,8 @@ public function testConstructLoggedOut()
/** /**
* Attempt to instantiate a LinkDB whereas the datastore is not writable * Attempt to instantiate a LinkDB whereas the datastore is not writable
* *
* @expectedException PHPUnit_Framework_Error_Warning * @expectedException IOException
* @expectedExceptionMessageRegExp /failed to open stream: No such file or directory/ * @expectedExceptionMessageRegExp /Error accessing null/
*/ */
public function testConstructDatastoreNotWriteable() public function testConstructDatastoreNotWriteable()
{ {