. */ class nkException extends Exception { } class nkPluginException extends nkException { } class nkUserException extends nkException { } class nkTemplateException extends nkException { } class NanoKubbe { const VERSION = '2.0.0'; const COMMENT_CONTEXT_COMMENT = 0; const COMMENT_CONTEXT_PAGE = 1; const COMMENT_CONTEXT_MEDIA = 2; const URL_CONTEXT_HISTORY = 'history'; const URL_CONTEXT_COMMENTS = 'comments'; static private $instance = null; private $sessions_started = false; static protected $datas = false; private $plugins = false; static private $signals = array( 'url.call', 'url.notfound', 'page.create.before', 'page.edit.before', 'page.delete.before', 'page.create.after', 'page.edit.after', 'page.delete.after', 'comment.add.before', 'comment.delete.before', 'comment.add.after', 'comment.delete.after', 'media.create.before', 'media.edit.before', 'media.publish.before', 'media.delete.before', 'media.create.after', 'media.edit.after', 'media.publish.after', 'media.delete.after', ); public $allowed_html = array( // Inline 'strong'=> array('class'), 'em' => array('class'), 'sup' => array('class'), 'sub' => array('class'), 'span' => array('class'), 'abbr' => array('class', 'title'), 'acronym' => array('class', 'title'), 'a' => array('class', 'href', 'title'), 'code' => array('class'), 'cite' => array('class'), 'del' => array('class'), 'ins' => array('class'), 'kbd' => array('class'), 'samp' => array('class'), 'br' => array(), // Block 'div' => array('class', 'id'), 'p' => array('class'), 'blockquote' => array('class'), 'ul' => array('class'), 'li' => array('class'), 'ol' => array('class'), 'h1' => array('class', 'id'), 'h2' => array('class', 'id'), 'h3' => array('class', 'id'), 'h4' => array('class'), 'h5' => array('class'), 'h6' => array('class'), 'pre' => array('class'), 'dl' => array('class'), 'dt' => array('class'), 'dd' => array('class'), 'hr' => array('class'), // Table elements 'table' => array('class'), 'caption' => array('class'), 'thead' => array('class'), 'tbody' => array('class'), 'tfoot' => array('class'), 'tr' => array('class'), 'td' => array('class', 'rowspan', 'colspan'), 'th' => array('class', 'rowspan', 'colspan'), // Media elements 'img' => array('class', 'src', 'alt', 'width', 'height', 'title'), 'object'=> array('class', 'width', 'height', 'title', 'type', 'data'), 'param' => array('name', 'value'), 'embed' => array('src', 'type', 'width', 'height'), ); public function __construct() { $init = false; if (!file_exists(DATAS_PATH . '/datas.db')) $init = true; self::$datas = new SQLiteDatabase(DATAS_PATH . '/datas.db', 0600); $this->plugins = new SQLiteDatabase(DATAS_PATH . '/plugins.db', 0600); if ($init) { try { $this->initDatas(); } catch (nkException $e) { unlink(DATAS_PATH . '/datas.db'); unlink(DATAS_PATH . '/plugins.db'); throw $e; } } self::$instance = $this; } static public function &instance() { if (!self::$instance) new NanoKubbe; return self::$instance; } private function initDatas() { $sql = file_get_contents(BASE_PATH . '/include/datas-db-scheme.txt'); $sql = preg_replace('!(#|//).*!', '', $sql); // Deleting comments self::$datas->queryExec($sql); $sql = file_get_contents(BASE_PATH . '/include/plugins-db-scheme.txt'); $sql = preg_replace('!(#|//).*!', '', $sql); // Deleting comments $this->plugins->queryExec($sql); $dirs = array('plugins', 'pages', 'comments', 'medias'); for ($i = 0; $i < 99; $i++) { $nb = $i < 10 ? '0' . $i : $i; $dirs[] = 'pages/' . $nb; $dirs[] = 'comments/' . $nb; $dirs[] = 'medias/' . $nb; } foreach ($dirs as $dir) { if (!file_exists(DATAS_PATH . '/' . $dir)) mkdir(DATAS_PATH . '/' . $dir); if (!is_writeable(DATAS_PATH . '/' . $dir)) throw new nkException("Application can't write to " . DATAS_PATH . '/' . $dir); } $this->createPage('My home', 'en', '_home'); return true; } /************************************************************************************************ ** PLUGINS MANAGEMENT ** */ static private function getPluginPath($name, $file=null) { $path = DATAS_PATH . '/plugins/' . $name; if (!is_null($file)) { $path .= '/' . $file; } return $path; } public function emitSignal($signal, $args = array()) { $res = $this->plugins->arrayQuery('SELECT plugin, file, callback FROM signals WHERE signal = \'' . sqlite_escape_string($signal) . '\' ORDER BY plugin;', SQLITE_ASSOC); if (empty($res)) return false; array_unshift($args, $this); foreach ($res as $row) { require_once self::getPluginPath($row['plugin'], $row['file']); if (!empty($row['callback'])) { if (strpos($row['callback'], '::') !== false) { $row['callback'] = explode('::', $row['callback']); } if (call_user_func_array($row['callback'], $args)) { return true; } } } return false; } public function registerPlugin($plugin) { $signals = self::getPluginPath($plugin, 'signals.cfg'); $signals_to_add = array(); if (file_exists($signals)) { $signals = file($signals); foreach ($signals as $signal) { list($signal, $file, $callback) = explode(';', $signal); $signal = trim($signal); $file = trim($file); $callback = trim($callback); if (!in_array($signal, self::$signals)) throw new nkPluginException("Unknow signal '{$signal}'."); if (!preg_match('!^(?:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*::)?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$!', $callback) || !is_callable($callback, true)) throw new nkPluginException("Invalid callback '{$callback}' for signal {$signal}."); if (!file_exists(self::getPluginPath($plugin, $file))) throw new nkPluginException("Non-existent file '{$file}' for signal {$signal}."); $signals_to_add[] = array($signal, $file, $callback); } } $this->plugins->unbufferedQuery('DELETE FROM signals WHERE plugin = \''.sqlite_escape_string($plugin).'\';'); foreach ($signals_to_add as $signal) { list($signal, $file, $callback) = $signal; $this->plugins->unbufferedQuery('INSERT INTO signals (plugin, signal, file, callback) VALUES (\''.sqlite_escape_string($plugin).'\', \''.sqlite_escape_string($signal).'\', \''.sqlite_escape_string($file).'\', \''.sqlite_escape_string($callback).'\');'); } return true; } /*********************************************************************************************** ** USER MANAGEMENT ** */ private function _startSession() { if(!$this->sessions_started) { @session_start(); $this->sessions_started = true; } } public function login($login, $password) { if ($login != $this->getConfig('login')) return false; if ($password != $this->getConfig('password')) return false; $this->_startSession(); $_SESSION['is_logged'] = true; return true; } public function isLogged() { $this->_startSession(); if(!empty($_SESSION['is_logged'])) return true; return false; } public function logout() { $this->_startSession(); $_SESSION = array(); session_write_close(); return true; } public function getConfig($key) { $res = self::$datas->arrayQuery('SELECT type, value FROM config WHERE key=\''.sqlite_escape_string($key).'\';', SQLITE_ASSOC); if (empty($res[0])) return null; switch ($res[0]['type']) { case 'int': return (int) $res[0]['value']; case 'bool': return (bool) $res[0]['value']; default: return (string) $res[0]['value']; } } public function setConfig($key, $value) { $res = self::$datas->arrayQuery('SELECT type FROM config WHERE key=\''.sqlite_escape_string($key).'\';', SQLITE_ASSOC); if (empty($res[0])) throw new nkException('Unable to set illegal config key '.$key); self::$datas->unbufferedQuery('UPDATE config SET value=\''.sqlite_escape_string($value).'\' WHERE key=\''.sqlite_escape_string($key).'\';'); return true; } private function getRevisionFilename($id, $rev) { return DATAS_PATH . '/pages/' . substr($id, -2) . '/' . $id . '/r' . $rev . '.txt'; } public function getPagesIdsFromUri($uri) { $res = self::$datas->arrayQuery('SELECT id, lang FROM pages WHERE uri = \''.sqlite_escape_string($uri).'\';', SQLITE_ASSOC); $pages = array(); foreach ($res as $row) { $pages[$row['lang']] = $row['id']; } return $pages; } public function isValidPageUri($uri) { if (preg_match('!^[A-Za-z0-9_-]+$!', $uri)) return true; else return false; } public function checkPageUri($id=false, $uri, $lang) { $pages = $this->getPagesIdsFromUri($uri); if (isset($pages[$lang]) && (!$id || $pages[$lang] != $id)) { return false; } return true; } public function checkPageExist($id) { $res = self::$datas->arrayQuery('SELECT 1 FROM pages WHERE id = \''.(int)$id.'\';', SQLITE_NUM); return !empty($res[0][0]) ? true : false; } public function getPage($id, $rev=0) { $res = self::$datas->arrayQuery('SELECT * FROM pages WHERE id = \''.(int)$id.'\';', SQLITE_ASSOC); if (empty($res)) return false; $page =& $res[0]; if (empty($rev)) $rev = $page['revision']; if ($rev > 0) $page['content'] = file_get_contents($this->getRevisionFilename($id, $rev)); else $page['content'] = ''; return $page; } public function getPageRevisionsList($id) { $res = self::$datas->arrayQuery('SELECT * FROM revisions WHERE page = \''.(int)$id.'\' ORDER BY id DESC;', SQLITE_ASSOC); return empty($res) ? false : $res; } public function createPage($title, $lang, $uri='', $online=true) { $this->emitSignal('page.create.before', array(&$title, &$lang, &$uri)); if (empty($uri)) $uri = utils::makeUriFromTitle($title); else $uri = utils::makeUriFromTitle($uri); if (!$this->checkPageUri(false, $uri, $lang)) $uri = utils::id2url(time()) . '-' . $uri; if ($uri == '_home') $id = '100'; else $id = 'NULL'; self::$datas->unbufferedQuery('INSERT INTO pages (id, title, uri, date_create, date_update, year, month, day, lang, status) VALUES ('.$id.', \''.sqlite_escape_string($title).'\', \''.sqlite_escape_string($uri).'\', \''.time().'\', \''.time().'\', \''.date('Y').'\', \''.date('m').'\', \''.date('d').'\', \''.sqlite_escape_string($lang).'\', \''.(int)$online.'\');'); $id = self::$datas->lastInsertRowid(); mkdir(DATAS_PATH . '/pages/' . substr($id, -2) . '/' . $id); $this->emitSignal('page.create.after', array(&$title, &$lang, &$uri, &$id)); return array($id, $uri); } public function editPage($id, $page, $comment='') { $this->emitSignal('page.edit.before', array(&$id, &$page, &$comment)); if (empty($page['uri'])) { $page['uri'] = $page['title']; } $page['uri'] = utils::makeUriFromTitle($page['uri']); if (!$this->checkPageUri($id, $page['uri'], $page['lang'])) $uri = utils::id2url($id) . '-' . $page['uri']; $page['content'] = trim($page['content']); $current = $this->getPage($id); if ($current['content'] == $page['content']) $page['revision'] = $current['revision']; else $page['revision'] = $current['revision'] + 1; self::$datas->unbufferedQuery('UPDATE pages SET title = \''.sqlite_escape_string($page['title']).'\', uri = \''.sqlite_escape_string($page['uri']).'\', revision = \''.sqlite_escape_string($page['revision']).'\', parent = \''.(int)$page['parent'].'\', allow_comments = \''.(int)$page['allow_comments'].'\', lang = \''.sqlite_escape_string($page['lang']).'\', date_update = \''.time().'\', status = \''.(int)$page['status'].'\' WHERE id = \''.(int)$id.'\';'); if ($page['revision'] != $current['revision']) { file_put_contents($this->getRevisionFilename($id, $page['revision']), $page['content']); $diff = strlen($current['content']) - strlen($page['content']); if ($diff > 0) $diff = '+' . $diff; self::$datas->unbufferedQuery('INSERT INTO revisions (page, id, date, comment, changed) VALUES (\''.(int)$id.'\', \''.(int)$page['revision'].'\', \''.time().'\', \''.sqlite_escape_string($comment).'\', \''.sqlite_escape_string($diff).'\');'); } $this->emitSignal('page.edit.after', array(&$id, &$page, &$comment)); return true; } public function deletePage($id) { $this->emitSignal('page.delete.before', array(&$id)); utils::recursiveDelete(DATAS_PATH . '/pages/' . substr($id, -2) . '/' . $id); self::$datas->unbufferedQuery('DELETE FROM revisions WHERE page = \''.(int)$id.'\';'); self::$datas->unbufferedQuery('DELETE FROM comments WHERE parent = \''.(int)$id.'\' AND context = \''.NanoKubbe::COMMENT_CONTEXT_PAGE.'\';'); self::$datas->unbufferedQuery('DELETE FROM medias WHERE page = \''.(int)$id.'\';'); self::$datas->unbufferedQuery('DELETE FROM pages WHERE parent = \''.(int)$id.'\';'); self::$datas->unbufferedQuery('DELETE FROM pages WHERE id = \''.(int)$id.'\';'); $this->emitSignal('page.delete.after', array(&$id)); return true; } public function getPagesCount($parent=0) { $res = self::$datas->arrayQuery('SELECT COUNT(id) FROM pages WHERE parent=\''.(int)$parent.'\';', SQLITE_NUM); return $res[0][0]; } public function getPagesList($order=false, $parent=0, $begin=0, $limit=50) { if ($order == self::BY_LAST_UPDATE) $o = 'date_update DESC'; elseif ($order == self::BY_CREATION) $o = 'date_create DESC'; else $o = 'title ASC'; return self::$datas->arrayQuery('SELECT * FROM pages WHERE parent=\''.(int)$parent.'\' ORDER BY '.$o.' LIMIT '.(int)$begin.','.(int)$limit.';', SQLITE_ASSOC); } //public function getPagesListByLang() public function getPageHistoryUrl($id) { return $this->getPageUrl($id, null, null, self::URL_CONTEXT_HISTORY); } public function getPageUrl($id=null, $uri=null, $lang=null, $last=null) { $url = utils::getBaseUrl(); if (($uri == '_home' || !$id) && !$uri && !$lang && !$last) return $url; if ($id) $url.= '-'.utils::id2url($id).'-/'; if ($uri) $url.= $uri . '/'; if ($lang) $url.= $lang . '/'; if (is_int($last) && $last > 0) { $url.= 'r'.(int)$last; } elseif (is_string($last)) { if ($last == self::URL_CONTEXT_HISTORY) { $url .= 'history'; } elseif ($last == self::URL_CONTEXT_COMMENTS) { $url .= 'comments'; } } return $url; } public function getMimeType($ext) { switch ($ext) { case 'css': return 'text/css'; case 'csv': return 'text/comma-separated-values'; case 'html': case 'htm': case 'xhtml': case 'tpl': return 'text/html'; case 'jpeg': case 'jpe': case 'jpg': return 'image/jpeg'; case 'gif': return 'image/gif'; case 'png': return 'image/png'; case 'mng': return 'image/mng'; case 'svg': return 'image/svg+xml'; case 'php': case 'php3': case 'phtml': return 'text/plain'; case 'mp3': return 'audio/mp3'; case 'ogg': return 'audio/x-ogg'; case 'ogm': return 'application/x-ogg'; case 'avi': return 'video/avi'; case 'pdf': return 'application/pdg'; case 'txt': return 'text/plain'; case 'js': return 'text/javascript'; default: return 'unknown'; } } } ?>