diff --git a/source/lib/SihnonFramework/Auth.class.php b/source/lib/SihnonFramework/Auth.class.php new file mode 100644 index 0000000..383168b --- /dev/null +++ b/source/lib/SihnonFramework/Auth.class.php @@ -0,0 +1,122 @@ +config = $config; + $this->session = $session; + $this->authenticated = false; + + $this->init($this->config->get('auth')); + } + + protected function init($backend) { + $this->backend = Sihnon_Auth_PluginFactory::create($this->config, $backend); + $this->restoreSession(); + } + + public function isAuthenticated() { + return $this->authenticated; + } + + public function authenticatedUser() { + return $this->user; + } + + public function saveSession() { + if ($this->user) { + $this->session->set('user.id', $this->user->username()); + } + } + + public function clearSession() { + $this->session->delete('user.id'); + } + + public function restoreSession() { + if ($this->session->exists('user.id')) { + $this->user = $this->backend->authenticateSession($this->session->get('user.id')); + $this->authenticated = true; + } + } + + public function register($username, $password) { + $this->user = $this->addUser($username, $password); + $this->authenticated = true; + } + + /* + * IPlugin methods + */ + + public function listUsers() { + return $this->backend->listUsers(); + } + + public function authenticate($username, $password) { + $this->user = $this->backend->authenticate($username, $password); + $this->authenticated = true; + + $this->session->securityLeveLChanged(); + $this->saveSession(); + } + + public function deauthenticate() { + $this->user = null; + $this->authenticated = false; + + $this->clearSession(); + } + + /* + * IUpdateable methods + */ + + public function addUser($username, $password) { + return $this->backend->addUser($username, $password); + } + + public function removeUser() { + $this->backend->removeUser($this->user); + $this->user = null; + $this->authenticated = false; + } + + public function changePassword($new_password) { + $this->backend->changePassword($this->user, $new_password); + } + + /* + * IPermissionable methods + */ + + public function isAdministrator() { + if ( ! $this->user) { + return false; + } + + return $this->backend->isAdministrator($this->user); + } + + /* + * IFinelyPermissionable methods + */ + + public function hasPermission($permission) { + if ( ! $this->user) { + return false; + } + + return $this->backend->hasPermission($this->user, $permission); + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/IFinelyPermissionable.class.php b/source/lib/SihnonFramework/Auth/IFinelyPermissionable.class.php new file mode 100644 index 0000000..3f291c0 --- /dev/null +++ b/source/lib/SihnonFramework/Auth/IFinelyPermissionable.class.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/IPermissionable.class.php b/source/lib/SihnonFramework/Auth/IPermissionable.class.php new file mode 100644 index 0000000..75abc3c --- /dev/null +++ b/source/lib/SihnonFramework/Auth/IPermissionable.class.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/IPlugin.class.php b/source/lib/SihnonFramework/Auth/IPlugin.class.php new file mode 100644 index 0000000..26dd425 --- /dev/null +++ b/source/lib/SihnonFramework/Auth/IPlugin.class.php @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/IUpdateable.class.php b/source/lib/SihnonFramework/Auth/IUpdateable.class.php new file mode 100644 index 0000000..971c956 --- /dev/null +++ b/source/lib/SihnonFramework/Auth/IUpdateable.class.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/IUser.class.php b/source/lib/SihnonFramework/Auth/IUser.class.php new file mode 100644 index 0000000..5ed5814 --- /dev/null +++ b/source/lib/SihnonFramework/Auth/IUser.class.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/Plugin/Database.class.php b/source/lib/SihnonFramework/Auth/Plugin/Database.class.php new file mode 100644 index 0000000..6200d0b --- /dev/null +++ b/source/lib/SihnonFramework/Auth/Plugin/Database.class.php @@ -0,0 +1,87 @@ +config = $config; + $this->database = SihnonFramework_Main::instance()->database(); + } + + /* + * IPlugin methods + */ + + public static function create(SihnonFramework_Config $config) { + return new self($config); + } + + public function userExists($username) { + return Sihnon_Auth_Plugin_Database_User::exists($username); + } + + public function listUsers() { + return Sihnon_Auth_Plugin_Database_User::all(); + } + + public function authenticate($username, $password) { + try { + $user = Sihnon_Auth_Plugin_Database_User::from('username', $username); + } catch (Sihnon_Exception_ResultCountMismatch $e) { + throw new Sihnon_Exception_UnknownUser(); + } + + if ( ! $user->checkPassword($password)) { + throw new Sihnon_Exception_IncorrectPassword(); + } + + return $user; + } + + public function authenticateSession($username) { + return Sihnon_Auth_Plugin_Database_User::from('username', $username); + } + + /* + * IUpdateable methods + */ + + public function addUser($username, $password) { + return Sihnon_Auth_Plugin_Database_User::add($username, $password); + } + + public function removeUser(Sihnon_Auth_IUser $user) { + $user->delete(); + } + + public function changePassword(Sihnon_Auth_IUser $user, $new_password) { + $user->changePassword($new_password); + } + + /* + * IPermissionable methods + */ + + public function isAdministrator(Sihnon_Auth_IUser $user) { + // As this class supports fine-grained permissions, map the isAdministrator function to the Superadmin privilege + // to fallback for badly written applications + return $user->hasPermission(Sihnon_Auth_Plugin_Database_Permission::PERM_Administrator); + } + + /* + * IFinelyPermissionable methods + */ + + public function hasPermission(Sihnon_Auth_IUser $user, $permission) { + return $user->hasPermission($permission); + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/Plugin/Database/Group.class.php b/source/lib/SihnonFramework/Auth/Plugin/Database/Group.class.php new file mode 100644 index 0000000..b707c40 --- /dev/null +++ b/source/lib/SihnonFramework/Auth/Plugin/Database/Group.class.php @@ -0,0 +1,12 @@ +username = $username; + $user->password = sha1($password); + $user->last_password_change = time(); + $user->create(); + + return $user; + } + + public function username() { + return $this->username; + } + + public function checkPassword($password) { + return ($this->password == sha1($password)); + } + + public function changePassword($new_password) { + $this->password = sha1($new_password); + $this->save(); + } + + public function groups($ignore_cache = false) { + if ($this->groups === null || $ignore_cache) { + $this->groups = Sihnon_Auth_Plugin_Database_Group::allFor('user', $this->id, 'groups_by_user'); + } + + return $this->groups; + } + + public function permissions($ignore_cache = false) { + if ($this->permissions === null || $ignore_cache) { + $this->permissions = Sihnon_Auth_Plugin_Database_Permission::allFor('user', $this->id, 'permissions_by_user'); + } + + return $this->permissions; + } + + public function hasPermission($permission) { + $permissions = $this->permissions(); + foreach ($permissions as $has_permission) { + if ($permission == $has_permission->id) { + return true; + } + } + + return false; + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/Plugin/Database/UserGroup.class.php b/source/lib/SihnonFramework/Auth/Plugin/Database/UserGroup.class.php new file mode 100644 index 0000000..83ef191 --- /dev/null +++ b/source/lib/SihnonFramework/Auth/Plugin/Database/UserGroup.class.php @@ -0,0 +1,13 @@ +config = $config; + } + + /* + * IPlugin methods + */ + + public static function create(SihnonFramework_Config $config) { + return new self($config); + } + + public function userExists($username) { + return Sihnon_Auth_Plugin_FlatFile_User::exists($username); + } + + public function listUsers() { + return Sihnon_Auth_Plugin_FlatFile_User::all(); + } + + public function authenticate($username, $password) { + $user = Sihnon_Auth_Plugin_FlatFile_User::load($username); + + if ( ! $user->checkPassword($password)) { + throw new Sihnon_Exception_IncorrectPassword(); + } + + return $user; + } + + public function authenticateSession($username) { + return Sihnon_Auth_Plugin_FlatFile_User::load($username); + } + + /* + * IUpdateable methods + */ + + public function addUser($username, $password) { + return Sihnon_Auth_Plugin_Database_User::add($username, $password); + } + + public function removeUser(Sihnon_Auth_IUser $user) { + $user->delete(); + } + + public function changePassword(Sihnon_Auth_IUser $user, $new_password) { + $user->changePassword($new_password); + } + + /* + * IPermissionable methods + */ + + public function isAdministrator(Sihnon_Auth_IUser $user) { + return $user->isAdministrator(); + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/Plugin/FlatFile/User.class.php b/source/lib/SihnonFramework/Auth/Plugin/FlatFile/User.class.php new file mode 100644 index 0000000..4f5a80f --- /dev/null +++ b/source/lib/SihnonFramework/Auth/Plugin/FlatFile/User.class.php @@ -0,0 +1,35 @@ +username; + } + + public function checkPassword($password) { + return ($this->password == sha1($password)); + } + + public function changePassword($new_password) { + throw new SihnonFramework_Exception_NotImplemented(); + } + + public function isAdministrator() { + throw new SihnonFramework_Exception_NotImplemented(); + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Auth/PluginFactory.class.php b/source/lib/SihnonFramework/Auth/PluginFactory.class.php new file mode 100644 index 0000000..74286c5 --- /dev/null +++ b/source/lib/SihnonFramework/Auth/PluginFactory.class.php @@ -0,0 +1,30 @@ + 'SihnonFramework/Auth/Plugin/', + Sihnon_Lib => 'Sihnon/Auth/Plugin/', + ); + + public static function init() { + + } + + public static function create(SihnonFramework_Config $config, $plugin) { + self::ensureScanned(); + + if (! self::isValidPlugin($plugin)) { + throw new Sihnon_Exception_InvalidPluginName($plugin); + } + + $classname = self::classname($plugin); + + return call_user_func(array($classname, 'create'), $config); + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/BackgroundTask.class.php b/source/lib/SihnonFramework/BackgroundTask.class.php index 90c057f..69491e0 100644 --- a/source/lib/SihnonFramework/BackgroundTask.class.php +++ b/source/lib/SihnonFramework/BackgroundTask.class.php @@ -6,10 +6,10 @@ class SihnonFramework_BackgroundTask { } - public static function run($command) { + public static function run($command, $cwd=null, $env=null) { SihnonFramework_LogEntry::debug(SihnonFramework_Main::instance()->log(), "Running background task: {$command} &", 'default'); $pipes = array(); - $pid = proc_open($command . ' &', array(), $pipes); + $pid = proc_open($command . ' &', array(), $pipes, $cwd, $env); proc_close($pid); } diff --git a/source/lib/SihnonFramework/Config.class.php b/source/lib/SihnonFramework/Config.class.php index 50a84e6..1f804ab 100644 --- a/source/lib/SihnonFramework/Config.class.php +++ b/source/lib/SihnonFramework/Config.class.php @@ -32,6 +32,12 @@ class SihnonFramework_Config { */ const TYPE_STRING_LIST = 'array(string)'; + /** + * Hash type with string keys and mixed-type values + * @var array(string=>mixed) + */ + const TYPE_HASH = 'hash'; + /** * Backend to be used for this Config object * @var Sihnon_Config_IPlugin @@ -58,9 +64,11 @@ class SihnonFramework_Config { protected static function pack($type, $value) { switch ($type) { - case static::TYPE_STRING_LIST: { + case static::TYPE_STRING_LIST: return join("\n", $value); - } break; + + case static::TYPE_HASH: + return join("\n", array_map(function($k, $v) { return "{$k}:{$v}"; }, array_keys($value), array_values($value))); default: { return $value; @@ -70,9 +78,17 @@ class SihnonFramework_Config { protected static function unpack($type, $value) { switch ($type) { - case self::TYPE_STRING_LIST: + case static::TYPE_STRING_LIST: + // foo + // bar return array_map('trim', explode("\n", $value)); + case static::TYPE_HASH: + // foo:bar + // baz:quz + preg_match_all("/^([^:]+):(.+)$/m", $value, $pairs); + return array_combine($pairs[1], $pairs[2]); + default: return $value; } diff --git a/source/lib/SihnonFramework/Daemon.class.php b/source/lib/SihnonFramework/Daemon.class.php new file mode 100644 index 0000000..0fc3d5b --- /dev/null +++ b/source/lib/SihnonFramework/Daemon.class.php @@ -0,0 +1,50 @@ +config = $config; + $this->lock_file = $config->get('daemon.lock-file'); + $this->lock = null; + $this->locked = false; + + $this->init(); + } + + public function __destruct() { + $this->teardown(); + } + + protected function init() { + $this->lock = fopen($this->lock_file, 'w'); + $wouldBlock = false; + + $result = flock($this->lock, LOCK_EX|LOCK_NB, $wouldBlock); + if ($wouldBlock) { + // Another instance is already running + throw new SihnonFramework_Exception_AlreadyRunning(); + } else if ( ! $result) { + throw new SihnonFramework_Exception_LockingFailed(); + } + + + } + + protected function teardown() { + if ( ! $this->locked) { + return; + } + + flock($this->lock, LOCK_UN); + fclose($this->lock); + unlink($this->lock_file); + } +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Database.class.php b/source/lib/SihnonFramework/Database.class.php index 9de43f6..57f596c 100644 --- a/source/lib/SihnonFramework/Database.class.php +++ b/source/lib/SihnonFramework/Database.class.php @@ -68,7 +68,7 @@ class SihnonFramework_Database { } private function query($sql, $count = 0) { - $results = $this->dbh->query($sql); + $results = $this->dbh->query($sql, PDO::FETCH_ASSOC); if (! $results) { list($std_code, $driver_code, $message) = $this->dbh->errorInfo(); @@ -123,7 +123,7 @@ class SihnonFramework_Database { } } - return $stmt->fetchAll(); + return $stmt->fetchAll(PDO::FETCH_ASSOC); } else { $results = array(); diff --git a/source/lib/SihnonFramework/DatabaseObject.class.php b/source/lib/SihnonFramework/DatabaseObject.class.php new file mode 100644 index 0000000..c4fd3bd --- /dev/null +++ b/source/lib/SihnonFramework/DatabaseObject.class.php @@ -0,0 +1,227 @@ + $value) { + if (property_exists(get_called_class(), '_db_' . $property)) { + $this->{'_db_' . $property} = $value; + } else { + throw new Sihnon_Exception_InvalidProperty($property); + } + } + } + + public static function fromDatabaseRow($row) { + $object = new static(); + $object->setDatabaseProperties($row); + + return $object; + } + + /** + * Load a DatabaseObject given its ID + * + * @param int $id + * @return SihnonFramework_DatabaseObject + */ + public static function fromId($id) { + $database = SihnonFramework_Main::instance()->database(); + + $object = self::fromDatabaseRow( + $database->selectOne('SELECT * FROM `'.static::table().'` WHERE id=:id', array( + array('name' => 'id', 'value' => $id, 'type' => PDO::PARAM_INT) + ) + ) + ); + + return $object; + } + + /** + * Load a DatabaseObject given a field name and value + * + * @param string $name Name of the field to match on + * @param mixed $value Value of the field to match on + */ + public static function from($field, $value) { + $database = SihnonFramework_Main::instance()->database(); + + $object = self::fromDatabaseRow( + $database->selectOne("SELECT * FROM `".static::table()."` WHERE `{$field}`=:{$field}", array( + array('name' => $field, 'value' => $value, 'type' => PDO::PARAM_STR) + ) + ) + ); + + return $object; + } + + /** + * Load a list of all objects + * + * @return SihnonFramework_DatabaseObject + */ + public static function all($view = null, $additional_conditions = null, $additional_params = null) { + $database = SihnonFramework_Main::instance()->database(); + + if ($view === null) { + $view = static::table(); + } + + $class = new ReflectionClass(get_called_class()); + $properties = $class->getproperties(); + + $fields = array(); + foreach ($properties as $property) { + $matches = array(); + if (preg_match('/^_db_(.*)/', $property->name, $matches)) { + $fields[] = $matches[1]; + } + } + + $field_list = join(', ', array_map(function($v) { return "`{$v}`"; }, $fields)); + + $params = array(); + if ($additional_params) { + $params = array_merge($params, $additional_params); + } + + $conditions = ''; + if ($additional_conditions) { + $conditions = "AND ({$additional_conditions}) "; + } + + $objects = array(); + $sql = "SELECT {$field_list} FROM `{$view}` WHERE `id` > 0 {$conditions} ORDER BY `id` DESC"; + foreach ($database->selectList($sql, $params) as $row) { + $objects[] = static::fromDatabaseRow($row); + } + + return $objects; + } + + public static function allFor($field, $value, $view = null, $additional_conditions = null, $additional_params = null) { + $conditions = "`{$field}`=:{$field} "; + if ($additional_conditions) { + $conditions .= "AND ({$additional_conditions}) "; + } + + $params = array( + array('name' => $field, 'value' => $value, 'type' => PDO::PARAM_STR), + ); + if ($additional_params) { + $params = array_merge($params, $additional_params); + } + + return static::all($view, $conditions, $params); + } + + public static function exists($field, $value, $view = null) { + $database = Sihnon_Main::instance()->database(); + + return $database->selectOne('SELECT COUNT(*) FROM `'.static::table().'` "WHERE `{$field}`=:{$field} LIMIT 1', array( + array('name' => $field, 'value' => $value, 'type' => PDO::PARAM_STR), + ) + ); + } + + protected function create() { + $database = SihnonFramework_Main::instance()->database(); + + $class = new ReflectionClass(get_called_class()); + $properties = $class->getproperties(); + + $fields = array(); + $params = array(); + foreach ($properties as $property) { + $matches = array(); + if (preg_match('/^_db_(.*)/', $property->name, $matches)) { + $fields[] = $matches[1]; + + $params[] = array( + 'name' => $matches[1], + 'value' => ($matches[1] == 'id') ? 'NULL' : $this->{"_db_{$matches[1]}"}, + 'type' => PDO::PARAM_STR + ); + } + } + + $id_list = join(', ', array_map(function($v) { return "`{$v}`"; }, $fields)); + $value_list = join(', ', array_map(function($v) { return ":{$v}"; }, $fields)); + + $database->insert("INSERT INTO `".static::table()."` ({$id_list}) VALUES({$value_list})", $params); + + $this->id = $database->lastInsertId(); + } + + public function save() { + $database = SihnonFramework_Main::instance()->database(); + + $class = new ReflectionClass(get_called_class()); + $properties = $class->getproperties(); + + $fields = array(); + $params = array(); + foreach ($properties as $property) { + $matches = array(); + if (preg_match('/^_db_(.*)/', $property->name, $matches)) { + if ($matches[1] != 'id') { + $fields[] = $matches[1]; + } + + $params[] = array( + 'name' => $matches[1], + 'value' => $this->{"_db_{$matches[1]}"}, + 'type' => PDO::PARAM_STR + ); + } + } + + $id_list = join(', ', array_map(function($v) { return "`{$v}`=:{$v}"; }, $fields)); + $value_list = join(', ', array_map(function($v) { return ":{$v}"; }, $fields)); + + $database->update("UPDATE `".static::table()."` SET {$id_list} WHERE `id`=:id", $params); + } + + public function delete() { + $database = SihnonFramework_Main::instance()->database(); + + $database->update( + 'DELETE FROM `'.static::table().'` WHERE `id`=:id LIMIT 1', + array( + array('name' => 'id', 'value' => $this->id, 'type' => PDO::PARAM_INT), + ) + ); + + $this->id = null; + } + + public function __set($name, $value) { + $fullname = "_db_{$name}"; + if ( ! property_exists(get_called_class(), $fullname)){ + throw new SihnonFramework_Exception_InvalidProperty($name); + } + + $this->{$fullname} = $value; + } + + public function __get($name) { + $fullname = "_db_{$name}"; + if ( ! property_exists(get_called_class(), $fullname)){ + throw new SihnonFramework_Exception_InvalidProperty($name); + } + + return $this->{$fullname}; + } + + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/DateTime.class.php b/source/lib/SihnonFramework/DateTime.class.php new file mode 100644 index 0000000..8de5602 --- /dev/null +++ b/source/lib/SihnonFramework/DateTime.class.php @@ -0,0 +1,119 @@ + 'one', + 2 => 'two', + 3 => 'three', + 4 => 'four', + 5 => 'five', + 6 => 'six', + 7 => 'seven', + 8 => 'eight', + 9 => 'nine', + 10 => 'ten', + 11 => 'eleven' + ); + + // today + if ($sod_now == $sod) { + if ($time > $now - (self::MINUTE * 3)) { + return 'just a moment ago'; + } else if ($time > $now - (self::MINUTE * 7)) { + return 'a few minutes ago'; + } else if ($time > $now - (self::HOUR)) { + return 'less than an hour ago'; + } + return 'today at ' . date('g:ia', $time); + } + + // yesterday + if (($sod_now - $sod) <= self::DAY) { + if (date('i', $time) > (self::MINUTE + 30)) { + $time += self::HOUR / 2; + } + return 'yesterday around ' . date('ga', $time); + } + + // within the last 5 days + if (($sod_now - $sod) <= (self::DAY * 5)) { + $str = date('l', $time); + $hour = date('G', $time); + if ($hour < 12) { + $str .= ' morning'; + } else if ($hour < 17) { + $str .= ' afternoon'; + } else if ($hour < 20) { + $str .= ' evening'; + } else { + $str .= ' night'; + } + return $str; + } + + // number of weeks (between 1 and 3)... + if (($sod_now-$sod) < (self::WEEK * 3.5)) { + if (($sod_now-$sod) < (self::WEEK * 1.5)) { + return 'about a week ago'; + } else if (($sod_now-$sod) < (self::DAY * 2.5)) { + return 'about two weeks ago'; + } else { + return 'about three weeks ago'; + } + } + + // number of months (between 1 and 11)... + if (($sod_now-$sod) < (self::MONTH * 11.5)) { + for ($i = (self::WEEK * 3.5), $m=0; $i < self::YEAR; $i += self::MONTH, $m++) { + if ( ($sod_now-$sod) <= $i ) { + return 'about ' . $convert[$m] . ' month' . (($m>1)?'s':'') . ' ago'; + } + } + } + + // number of years... + for ($i = (self::MONTH * 11.5), $y=0; $i < (self::YEAR * 10); $i += self::YEAR, $y++) { + if (($sod_now-$sod) <= $i) { + return 'about ' . $convert[$y] . ' year' . (($y>1)?'s':'') . ' ago'; + } + } + + // more than ten years... + return 'more than ten years ago'; + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Exceptions.class.php b/source/lib/SihnonFramework/Exceptions.class.php index 0ce68c0..c67f4d3 100644 --- a/source/lib/SihnonFramework/Exceptions.class.php +++ b/source/lib/SihnonFramework/Exceptions.class.php @@ -13,6 +13,7 @@ class SihnonFramework_Exception_TemplateException extends SihnonFramework_E class SihnonFramework_Exception_AbortEntirePage extends SihnonFramework_Exception_TemplateException {}; class SihnonFramework_Exception_Unauthorized extends SihnonFramework_Exception_TemplateException {}; class SihnonFramework_Exception_FileNotFound extends SihnonFramework_Exception_TemplateException {}; +class SihnonFramework_Exception_NotAuthorised extends SihnonFramework_Exception_TemplateException {}; class SihnonFramework_Exception_InvalidParameters extends SihnonFramework_Exception_TemplateException {}; class SihnonFramework_Exception_DatabaseException extends SihnonFramework_Exception {}; @@ -21,6 +22,7 @@ class SihnonFramework_Exception_DatabaseConnectFailed extends SihnonFramework_E class SihnonFramework_Exception_NoDatabaseConnection extends SihnonFramework_Exception_DatabaseException {}; class SihnonFramework_Exception_DatabaseQueryFailed extends SihnonFramework_Exception_DatabaseException {}; class SihnonFramework_Exception_ResultCountMismatch extends SihnonFramework_Exception_DatabaseException {}; +class SihnonFramework_Exception_InvalidProperty extends SihnonFramework_Exception_DatabaseException {}; class SihnonFramework_Exception_ConfigException extends SihnonFramework_Exception {}; class SihnonFramework_Exception_UnknownSetting extends SihnonFramework_Exception_ConfigException {}; @@ -38,4 +40,16 @@ class SihnonFramework_Exception_LogException extends SihnonFramework_E class SihnonFramework_Exception_LogFileNotWriteable extends SihnonFramework_Exception_LogException {}; class SihnonFramework_Exception_InvalidLog extends SihnonFramework_Exception_LogException {}; +class SihnonFramework_Exception_AuthException extends SihnonFramework_Exception {}; +class SihnonFramework_Exception_UnknownUser extends SihnonFramework_Exception_AuthException {}; +class SihnonFramework_Exception_IncorrectPassword extends SihnonFramework_Exception_AuthException {}; + +class SihnonFramework_Exception_ValidationException extends SihnonFramework_Exception {}; +class SihnonFramework_Exception_InvalidContent extends SihnonFramework_Exception_ValidationException {}; +class SihnonFramework_Exception_InvalidLength extends SihnonFramework_Exception_ValidationException {}; + +class SihnonFramework_Exception_DaemonException extends SihnonFramework_Exception {}; +class SihnonFramework_Exception_AlreadyRunning extends SihnonFramework_Exception_DaemonException {}; +class SihnonFramework_Exception_LockingFailed extends SihnonFramework_Exception_DaemonException {}; + ?> diff --git a/source/lib/SihnonFramework/ForegroundTask.class.php b/source/lib/SihnonFramework/ForegroundTask.class.php index 3434c4f..837a2fc 100644 --- a/source/lib/SihnonFramework/ForegroundTask.class.php +++ b/source/lib/SihnonFramework/ForegroundTask.class.php @@ -35,7 +35,7 @@ class SihnonFramework_ForegroundTask { ); $pipes = array(); - $process = proc_open($command, $descriptors, $pipes); + $process = proc_open($command, $descriptors, $pipes, $cwd, $env); stream_set_blocking($pipes[self::PIPE_STDIN], 0); // Make stdin/stdout/stderr non-blocking stream_set_blocking($pipes[self::PIPE_STDOUT], 0); diff --git a/source/lib/SihnonFramework/Formatting.class.php b/source/lib/SihnonFramework/Formatting.class.php new file mode 100644 index 0000000..f8ce086 --- /dev/null +++ b/source/lib/SihnonFramework/Formatting.class.php @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Log/Plugin/Console.class.php b/source/lib/SihnonFramework/Log/Plugin/Console.class.php index 565e791..d56017a 100644 --- a/source/lib/SihnonFramework/Log/Plugin/Console.class.php +++ b/source/lib/SihnonFramework/Log/Plugin/Console.class.php @@ -34,6 +34,7 @@ class SihnonFramework_Log_Plugin_Console extends SihnonFramework_Log_PluginBase // Make some alterations for ease of display $fields_map['timestamp'] = date('d/m/y H:i:s', $fields_map['ctime']); + $fields_map['shortfile'] = basename($fields_map['file']); // split the map back out again now the modifications have been made $fields = array_keys($fields_map); diff --git a/source/lib/SihnonFramework/Main.class.php b/source/lib/SihnonFramework/Main.class.php index 0f6917b..1a17a0f 100644 --- a/source/lib/SihnonFramework/Main.class.php +++ b/source/lib/SihnonFramework/Main.class.php @@ -17,11 +17,14 @@ class SihnonFramework_Main { protected $database; protected $log; protected $cache; + protected $session; + protected $auth; protected $base_uri; protected function __construct() { - $this->base_uri = dirname($_SERVER['SCRIPT_NAME']) . '/'; + $dirname = dirname($_SERVER['SCRIPT_NAME']); + $this->base_uri = $dirname == '/' ? '/' : $dirname . '/'; } protected function init() { @@ -38,6 +41,9 @@ class SihnonFramework_Main { $this->log = new Sihnon_Log($this->config); $this->cache = new Sihnon_Cache($this->config); + + $this->session = new Sihnon_Session($this->config); + $this->auth = new Sihnon_Auth($this->config, $this->session); } /** @@ -86,6 +92,22 @@ class SihnonFramework_Main { return $this->cache; } + /** + * + * @return SihnonFramework_Session + */ + public function session() { + return $this->session; + } + + /** + * + * @return SihnonFramework_Auth + */ + public function auth() { + return $this->auth; + } + public function baseUri() { return $this->base_uri; } @@ -333,32 +355,74 @@ class SihnonFramework_Main { return $default; } - public static function formatDuration($time) { - if (is_null($time)) { - return 'unknown'; - } + public static function formatDuration($seconds, $fuzziness = 0) { + if (is_null($seconds)) { + return 'indeterminate time'; + } + + $labels = array('second', 'minute', 'hour', 'day', 'week', 'month', 'year'); + $pluralLabels = array('seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'); + $limits = array(1, 60, 3600, 86400, 604800, 2592000, 31556926, PHP_INT_MAX); + $components = array(0, 0, 0, 0, 0, 0, 0); + + $workingTime = $seconds; + + $result = ""; + $ptr = count($labels) - 1; - $labels = array('seconds', 'minutes', 'hours', 'days', 'weeks', 'months', 'years'); - $limits = array(1, 60, 3600, 86400, 604800, 2592000, 31556926, PHP_INT_MAX); + while ($ptr >= 0 && $workingTime < $limits[$ptr]) { + --$ptr; + } + $mostSignificantPtr = $ptr; + + // Convert the value into components using the remaining labels + while ($ptr >= 0) { + $unitTime = floor($workingTime / $limits[$ptr]); + $workingTime -= $unitTime * $limits[$ptr]; + $components[$ptr] = $unitTime; + --$ptr; + } + + $componentsUsed = 0; + $approximate = false; + $lastComponent = false; + $ptr = $mostSignificantPtr; + while ($ptr >= 0) { + if ($fuzziness && $componentsUsed >= $fuzziness) { + break; + } elseif ($ptr == 0 || ($fuzziness && $componentsUsed == $fuzziness-1)) { + $lastComponent = true; + } + + $component = $components[$ptr]; + if ($component) { + // If we're going to hide the next value, take its component into account here + if ($lastComponent && $ptr > 0) { + $component += round($components[$ptr-1] / $limits[$ptr]); + $approximate = true; + } + + if ($lastComponent && $ptr < $mostSignificantPtr) { + $result .= ' and'; + } + + $result .= ' ' . $component . ' ' . ($component == 1 ? $labels[$ptr] : $pluralLabels[$ptr]); + + } + + // Increment even if we've hidden this component because it's zero + // Then we don't end up with overly precise times like '2 years and 1 second' + ++$componentsUsed; + + --$ptr; + } - $working_time = $time; + if ($approximate) { + $result = 'approximately ' . $result; + } - $result = ""; - $ptr = count($labels) - 1; - - while ($ptr >= 0 && $working_time < $limits[$ptr]) { - --$ptr; - } - - while ($ptr >= 0) { - $unit_time = floor($working_time / $limits[$ptr]); - $working_time -= $unit_time * $limits[$ptr]; - $result = $result . ' ' . $unit_time . ' ' . $labels[$ptr]; - --$ptr; - } - - return $result; - } + return $result; + } public static function formatFilesize($bytes) { if (is_null($bytes)) { diff --git a/source/lib/SihnonFramework/Page.class.php b/source/lib/SihnonFramework/Page.class.php index ab8be43..8bd37d2 100644 --- a/source/lib/SihnonFramework/Page.class.php +++ b/source/lib/SihnonFramework/Page.class.php @@ -7,10 +7,14 @@ class SihnonFramework_Page { private $page; - public function __construct(Smarty $smarty, SihnonFramework_RequestParser $request) { + public function __construct(Smarty $smarty, SihnonFramework_RequestParser $request, $page = null) { $this->smarty = $smarty; $this->request = $request; - $this->page = $request->page(); + $this->page = $page; + + if ($page === null) { + $this->page = $request->page(); + } } public function page() { @@ -20,23 +24,34 @@ class SihnonFramework_Page { public function template_filename() { return $this->page . '.tpl'; } + + public function code_filename() { + return $this->page . '.php'; + } public function evaluate($template_variables = array()) { - $code_filename = $this->page . '.php'; + $code_filename = $this->code_filename(); $template_filename = $this->template_filename(); + $content = ''; try { - $this->render($template_filename, $code_filename, $template_variables); + $this->smarty->assign('page', $this); + $this->smarty->assign('requested_page', $this->page); + $content = $this->render($template_filename, $code_filename, $template_variables); } catch (SihnonFramework_Exception_AbortEntirePage $e) { return false; } catch (SihnonFramework_Exception_FileNotFound $e) { - $this->render('errors/404.tpl', 'errors/404.php'); + $content = $this->render('errors/404.tpl', 'errors/404.php'); + } catch (SihnonFramework_Exception_NotAuthorised $e) { + $content = $this->render('errors/401.tpl', 'errors/404.php'); } catch (SihnonFramework_Exception $e) { - $this->render('errors/unhandled-exception.tpl', 'errors/unhandled-exception.php', array( + $content = $this->render('errors/unhandled-exception.tpl', 'errors/unhandled-exception.php', array( 'exception' => $e, )); } + $this->smarty->assign('page_content', $content); + return true; } @@ -57,10 +72,13 @@ class SihnonFramework_Page { include $real_code_filename; } - $this->smarty->assign('requested_page', $this->page); - // Now execute the template itself, which will render the results of the code file - $this->smarty->assign('page_content', $this->smarty->fetch($template_filename)); + return $this->smarty->fetch($template_filename); + } + + public function include_template($page, $template_variables = array()) { + $subpage = new Sihnon_Page($this->smarty, $this->request, $page); + return $subpage->render($subpage->template_filename(), $subpage->code_filename(), $template_variables); } public static function redirect($relative_url) { diff --git a/source/lib/SihnonFramework/Session.class.php b/source/lib/SihnonFramework/Session.class.php new file mode 100644 index 0000000..b6b3ccb --- /dev/null +++ b/source/lib/SihnonFramework/Session.class.php @@ -0,0 +1,80 @@ +config = $config; + $this->enabled = false; + $this->dirty = false; + + if ($this->config->exists('sessions') && $this->config->get('sessions')) { + $this->enabled = true; + } + + $this->init(); + } + + public function __destruct() { + $this->teardown(); + } + + protected function init() { + if ($this->enabled) { + session_start(); + $this->state = $_SESSION; + + // Override the session parameters if configured + $params = session_get_cookie_params(); + $lifetime = $this->config->exists('sessions.lifetime') ? $this->config->get('sessions.lifetime') : $params['lifetime']; + $path = $this->config->exists('sessions.path') ? $this->config->get('sessions.path') : $params['path']; + $domain = $this->config->exists('sessions.domain') ? $this->config->get('sessions.domain') : $params['domain']; + $secure = $this->config->exists('sessions.secure') ? $this->config->get('sessions.secure') : $params['secure']; + $httponly = $this->config->exists('sessions.http-only') ? $this->config->get('sessions.http-only') : $params['httponly']; + session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly); + } + } + + protected function teardown() { + if ($this->enabled && $this->dirty) { + $_SESSION = $this->state; + session_write_close(); + } + } + + public function set($name, $value) { + $this->state[$name] = $value; + $this->dirty = true; + } + + public function get($name, $default = null) { + if ( ! $this->exists($name)) { + return $default; + } + + return $this->state[$name]; + } + + public function exists($name) { + return isset($this->state[$name]); + } + + public function delete($name) { + unset($this->state[$name]); + $this->dirty = true; + } + + public function securityLeveLChanged() { + if ($this->enabled) { + session_regenerate_id(true); + } + } + +} + +?> \ No newline at end of file diff --git a/source/lib/SihnonFramework/Validation.class.php b/source/lib/SihnonFramework/Validation.class.php new file mode 100644 index 0000000..f9ac15f --- /dev/null +++ b/source/lib/SihnonFramework/Validation.class.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Validation/Enum.class.php b/source/lib/SihnonFramework/Validation/Enum.class.php new file mode 100644 index 0000000..feecc5e --- /dev/null +++ b/source/lib/SihnonFramework/Validation/Enum.class.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/source/lib/SihnonFramework/Validation/Text.class.php b/source/lib/SihnonFramework/Validation/Text.class.php new file mode 100644 index 0000000..acb60c7 --- /dev/null +++ b/source/lib/SihnonFramework/Validation/Text.class.php @@ -0,0 +1,67 @@ + '[:alpha:]', + self::Digit => '[:digit:]', + self::Numeric => '[:digit:]\.-', + self::Symbol => '[:punct:]', + self::Whitespace => '[:space:]', + ); + + public static function content($inputs, $content = self::Defaults) { + static::pattern($inputs, static::buildContentPattern($content)); + } + + public static function length($inputs, $min_length = null, $max_length = null) { + if ( ! is_array($inputs)) { + $inputs = array($inputs); + } + + foreach ($inputs as $input) { + $length = strlen($input); + + if ($min_length !== null && $length < $min_length) { + throw new SihnonFramework_Exception_InvalidLength(); + } + + if ($max_length !== null && $length > $max_length) { + throw new SihnonFramework_Exception_InvalidLength(); + } + } + } + + public static function pattern($inputs, $pattern) { + if ( ! is_array($inputs)) { + $inputs = array($inputs); + } + + foreach ($inputs as $input) { + if ( ! preg_match($pattern, $input)) { + throw new SihnonFramework_Exception_InvalidContent(); + } + } + } + + protected static function buildContentPattern($contents) { + $classes = ''; + foreach (static::$contents as $set => $class) { + if ($contents & $set) { + $classes .= $class; + } + } + + return "/^[{$classes}]*$/"; + } + +} + +?> \ No newline at end of file