diff --git a/Action.class.php b/Action.class.php
new file mode 100644
index 0000000..69e2465
--- /dev/null
+++ b/Action.class.php
@@ -0,0 +1,101 @@
+class = get_class($this);
+ $this->template = substr($this->class, 0, -strlen(ACTION_POSTFIX));
+
+ if (CACHE_ENABLE && ($cache_name = $this->getCacheKey()) !== false) {
+ $cache = new Cache($this->class.'_'.$cache_name, $this->getCacheTime());
+ if ($cache->isCached()) {
+ $this->restore($cache->load());
+ } else {
+ $this->init();
+ $cache->save($this->store());
+ }
+ } else {
+ $this->init();
+ }
+ }
+
+ /**
+ * Выдает результат действия.
+ *
+ * @return string
+ */
+ public function display()
+ {
+ $this->templater = Load::templater(ACTION_TPL_PATH.'/'.$this->template_dir);
+ $this->prepare();
+ return $this->templater->fetch($this->template.'.tpl');
+ }
+
+ /**
+ * Инициализация данных действия.
+ * Тут должна быть ВСЯ работа с установкой данных,
+ * дабы потом они нормально закешировались.
+ * Этот метод НЕ исполняется при срабатывании кеша.
+ *
+ */
+ abstract protected function init();
+
+ /**
+ * Подготовка данных для шаблона.
+ * Этот метод выполняется ВСЕГДА
+ */
+ protected function prepare() {}
+
+ /**
+ * Возвращает имя файла для кеширования
+ * Переопределяется в доченрих классах.
+ *
+ * @return false/string - имя файла кеша либо false если кеш запрещен
+ */
+ protected function getCacheKey()
+ {
+ return false;
+ }
+
+ /**
+ * Возвращает время жизни кеша в секундах
+ * При отрицательном значении кеш вечен.
+ * Переопределяется в доченрих классах.
+ *
+ * @return false/integer - время жизни кеша либо false если кеш запрещен
+ */
+ protected function getCacheTime()
+ {
+ return 0;
+ }
+
+ /**
+ * Выдает строку для кеширования.
+ */
+ protected function store()
+ {
+ return serialize(get_object_vars($this));
+ }
+
+ /**
+ * Разбирает строку кеша, созданную методом store
+ */
+ protected function restore($data)
+ {
+ foreach(unserialize($data) as $key => $val) {
+ $this->$key = $val;
+ }
+ }
+}
+?>
\ No newline at end of file
diff --git a/Cache.class.php b/Cache.class.php
new file mode 100644
index 0000000..399926c
--- /dev/null
+++ b/Cache.class.php
@@ -0,0 +1,58 @@
+cache_time = (int) $cache_time;
+ $this->cache_file = CACHE_PATH.'/'.$cache_name;
+ }
+
+ /**
+ * Сохраняет кэш в файл
+ *
+ * @return boolean - сохранил или нет
+ */
+ function save($data)
+ {
+ if ($this->cache_time != 0) {
+ return (bool) file_put_contents($this->cache_file, $data);
+ }
+ return false;
+ }
+
+ /**
+ * Достает кэш из файла
+ *
+ * @return string - содержимое кеша
+ */
+ function load()
+ {
+ return file_get_contents($this->cache_file);
+ }
+
+ /**
+ * Проверяет, закешированы ли данные с таким именем
+ *
+ * @return boolean - закеширован или нет
+ */
+ function isCached()
+ {
+ if (! file_exists($this->cache_file)) {
+ return false;
+ }
+ if ($this->cache_time > 0 && $this->cache_time + filemtime($this->cache_file) < TIME_NOW) {
+ return false;
+ }
+ return true;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/DBConnector.class.php b/DBConnector.class.php
new file mode 100644
index 0000000..bbfb4a4
--- /dev/null
+++ b/DBConnector.class.php
@@ -0,0 +1,40 @@
+
\ No newline at end of file
diff --git a/Decorator.class.php b/Decorator.class.php
new file mode 100644
index 0000000..0fa8675
--- /dev/null
+++ b/Decorator.class.php
@@ -0,0 +1,55 @@
+layout) {
+ throw new MJException('$layout not set in '.get_class($this));
+ }
+ $this->templater = Load::templater();
+ }
+
+ /**
+ * Основной метод вывода
+ *
+ */
+ function display(Action $action)
+ {
+ $this->action = $action;
+ $this->templater->assign($this->action_name, $this->action->display(), $this->layout);
+
+ $this->exec();
+
+ $this->templater->setPath(WRAPPERS_TPL_PATH);
+ return $this->templater->fetch($this->layout);
+ }
+
+ /**
+ * Добавить данные на вывод
+ *
+ * @param string $var_name - имя переменной, в которую будет осуществлен вывод
+ * @param Action $action - действие
+ * @param Decorator $decorator - декоратор, в котором должно выполнится действие.
+ */
+ function addOutput($var_name, Action $action, Decorator $decorator = null)
+ {
+ $this->templater->assign($var_name, $decorator ? $decorator->display($action) : $action->display(), $this->layout);
+ }
+
+ /**
+ * Основной метод для дочерних декораторов
+ *
+ */
+ abstract function exec();
+}
+?>
\ No newline at end of file
diff --git a/Env.class.php b/Env.class.php
new file mode 100644
index 0000000..c2a7636
--- /dev/null
+++ b/Env.class.php
@@ -0,0 +1,64 @@
+
\ No newline at end of file
diff --git a/Load.class.php b/Load.class.php
new file mode 100644
index 0000000..07e584f
--- /dev/null
+++ b/Load.class.php
@@ -0,0 +1,35 @@
+setPath($path);
+ }
+ return self::$templater;
+ }
+ return self::$templater = new Sublimer($path);
+ }
+
+ static function router()
+ {
+ return self::$router ? self::$router : self::$router = new Router;
+ }
+}
+?>
\ No newline at end of file
diff --git a/MJException.class.php b/MJException.class.php
new file mode 100644
index 0000000..6db0916
--- /dev/null
+++ b/MJException.class.php
@@ -0,0 +1,57 @@
+MJ Error: ";
+ $return .= str_replace("\n", "
\n", $this->getMessage())."
\n";
+
+ $trace = $this->getTrace();
+
+ $file = reset($trace);
+
+ //смещение в трейсе, указаное при вызове эксепшена (2й параметр). Нужно для более инофрмативного вывода
+ if ($shift = abs($this->getCode())) {
+ while($shift--) {
+ $file = next($trace);
+ }
+ }
+
+ if ($fp = fopen($file['file'], 'r')) {
+ $error_line = $file['line'];
+ $start = $error_line - $this->line_range;
+ $end = $error_line + $this->line_range;
+ $i = 1;
+ $return .= "
";
+ while ($line = fgets($fp, 4096) and $i<=$end) {
+ $line = htmlspecialchars($line);
+ if ($i >= $start && $i <= $end) {
+ if ($i == $error_line) $return .= ''.$i.' '.$line.'
';
+ else $return .= $i.' '.$line;
+ }
+ $i++;
+ }
+ $return .= "
";
+ fclose($fp);
+ }
+
+ $return .= ' Backtrace';
+ $return .= "\n".$this->getFile().' | '.$this->getLine().' |
';
+ foreach($trace as $row) {
+ if (isset($row['file'])) { //throwing exception from __call method will not return file and line
+ $return .= "\n".$row['file'].' | '.$row['line'].' |
';
+ }
+ }
+ return $return . '
';
+ }
+}
+?>
diff --git a/Model.class.php b/Model.class.php
new file mode 100644
index 0000000..b5da5bf
--- /dev/null
+++ b/Model.class.php
@@ -0,0 +1,186 @@
+handler = DBConnector::getConnect(Env::getParam('db_settings'));
+ }
+
+ /**
+ * Выполняет запрос и возвращает сырой результат
+ *
+ * @param string $sql
+ * @return resource
+ */
+ function exec($sql)
+ {
+ $time = microtime(true);
+ $res = mysqli_query($this->handler, $sql);
+ if (mysqli_errno($this->handler)) {
+ throw new MJException("Query Error:\n".$sql."\nError:\n".mysqli_error($this->handler), 1);
+ }
+
+ if (DEBUG_ENABLE) {
+ DBConnector::$queries[] = $sql.'; ('.round((microtime(true)-$time)*1000, 1).'ms)';
+ }
+
+ return $res;
+ }
+
+ /**
+ * Выполняет запрос и возвращает объект результата
+ *
+ * @param string $sql
+ * @return object
+ */
+ function query($sql)
+ {
+ $res = $this->exec($sql);
+
+ switch (strtolower(substr($sql, 0, 6))) {
+ case 'select':
+ return new ModelSelectResult($res);
+ case 'insert':
+ return new ModelInsertResult($this->handler);
+ default:
+ return new ModelChangeResult($this->handler);
+ }
+ }
+
+ /**
+ * Экранирует строку
+ *
+ * @param string $data - строка для экранирования
+ * @return string
+ */
+ function escape($data)
+ {
+ return mysqli_real_escape_string($this->handler, $data);
+ }
+
+//////////////////////////
+ function update($id, $data)
+ {
+ $sql = '';
+ foreach ($data as $key => $val) {
+ $sql .= $key."='".$this->escape($val)."', ";
+ }
+ return $this->query('UPDATE '.$this->table.' SET '.rtrim($sql, ', ').' WHERE '.$this->primary_key.'='.(int) $id);
+ }
+
+ function insert($data, $postfix = '')
+ {
+ $sql = '';
+ foreach ($data as $key => $val) {
+ $sql .= $key.'="'.$this->escape($val).'", ';
+ }
+ return $this->query('INSERT '.$this->table.' SET '.rtrim($sql, ', ').' '.$postfix);
+ }
+
+ function delete($id)
+ {
+ return $this->query('DELETE FROM '.$this->table.' WHERE '.$this->primary_key.'='.(int) $id);
+ }
+
+ function get($id)
+ {
+ return $this->query('SELECT * FROM '.$this->table.' WHERE '.$this->primary_key.'='.(int) $id);
+ }
+
+ function getList($limit = false, $sort = 'ASC')
+ {
+ return $this->query('SELECT * FROM '.$this->table.' ORDER BY '.$this->primary_key.' '.($sort == 'ASC' ? 'ASC' : 'DESC').($limit === false ? ' LIMIT '.(int) $limit : ''));
+ }
+}
+
+class ModelResult
+{
+ function __call($name, $args)
+ {
+ throw new MJException('Call undeclared method "'.$name.'" in "'.get_class($this).'" class', -1);
+ }
+}
+
+class ModelSelectResult extends ModelResult
+{
+ public $result;
+
+ function __construct($res)
+ {
+ $this->result = $res;
+ }
+
+ function fetch()
+ {
+ return mysqli_fetch_object($this->result);
+ }
+
+ function fetchField($field, $default = false)
+ {
+ $row = $this->fetch();
+ return isset($row->$field) ? $row->$field : $default;
+ }
+
+ function fetchAll()
+ {
+ $array = array();
+ while ($row = mysqli_fetch_object($this->result)) {
+ $array[] = $row;
+ }
+ return $array;
+ }
+
+ function count()
+ {
+ return mysqli_num_rows($this->result);
+ }
+
+ function free()
+ {
+ mysqli_free_result($this->result);
+ }
+
+ function __destruct() {
+ $this->free();
+ }
+}
+
+class ModelChangeResult extends ModelResult
+{
+ public $affected;
+
+ function __construct($resource)
+ {
+ $this->affected = mysql_affected_rows($resource);
+ }
+
+ function count()
+ {
+ return $this->affected;
+ }
+}
+
+class ModelInsertResult extends ModelResult
+{
+ public $id;
+
+ function __construct($resource)
+ {
+ $this->id = mysqli_insert_id($resource);
+ }
+
+ function getId()
+ {
+ return $this->id;
+ }
+}
+?>
\ No newline at end of file
diff --git a/PageController.class.php b/PageController.class.php
new file mode 100644
index 0000000..caa88e6
--- /dev/null
+++ b/PageController.class.php
@@ -0,0 +1,27 @@
+route = Load::router()->proccess(MJ_PATH);
+ $decorator = new $this->route->decorator;
+ return $decorator->display(new $this->route->action);
+ } catch (MJException $e) {
+ return $e->terminate();
+ } catch (Exception $e) {
+ $decorator_name = DEFAULT_DECORATOR;
+ $action_name = DEFAULT_ACTION;
+
+ $decorator = new $decorator_name;
+ return $decorator->display(new $action_name($e->getMessage()));
+ }
+ }
+}
+?>
\ No newline at end of file
diff --git a/Router.class.php b/Router.class.php
new file mode 100644
index 0000000..c8d6373
--- /dev/null
+++ b/Router.class.php
@@ -0,0 +1,106 @@
+routes[$name] = new Route($path, $action, $params);
+ }
+
+ /**
+ * Установить декоратор для роута (действия), отличный от стандартного
+ *
+ * @param string $name - имя роута (действия)
+ * @param string $decorator - имя декоратора
+ */
+ function setDecorator($name, $decorator)
+ {
+ if (isset($this->routes[$name])) {
+ $this->routes[$name]->decorator = $decorator.DECORATOR_POSTFIX;
+ }
+ }
+
+ /**
+ * Найти роутер соответствующий заданному пути
+ *
+ * @param stirng $path - путь
+ * @return Route - роутер
+ */
+ function proccess($path)
+ {
+ $path = explode('/', $path);
+
+ foreach ($this->routes as $name => $route) {
+ if ($route->match($path)) {
+ $route->action .= ACTION_POSTFIX;
+ Env::setParams($route->params);
+ return $route;
+ }
+ }
+ throw new Exception(E_404);
+ }
+}
+
+/**
+ * Роутер
+ *
+ */
+final class Route
+{
+ const URL_VARIABLE = '&';
+
+ protected $path;
+
+ public $decorator = DEFAULT_DECORATOR;
+ public $action;
+ public $params;
+
+ function __construct($path, $action, $params=array())
+ {
+ $this->path = $path;
+ $this->action = $action;
+ $this->params = $params;
+ }
+
+ /**
+ * Проверяет соответствие роутера и пути
+ *
+ * @param string $path - путь для сравнения
+ * @return boolean - соответствует или нет
+ */
+ function match($path_arr)
+ {
+ $parts = explode('/', $this->path);
+ $cnt = count($parts);
+ if (end($parts) == self::URL_VARIABLE) {
+ $cnt--;
+ } elseif ($cnt != count($path_arr)) {
+ return false;
+ }
+
+ for ($i=0; $i<$cnt; $i++) {
+ if (substr($parts[$i], 0, 1) == self::URL_VARIABLE) {
+ $this->params[substr($parts[$i], 1)] = $path_arr[$i];
+ } elseif ($parts[$i] != $path_arr[$i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
+?>
\ No newline at end of file
diff --git a/Sublimer.class.php b/Sublimer.class.php
new file mode 100644
index 0000000..f79fb01
--- /dev/null
+++ b/Sublimer.class.php
@@ -0,0 +1,93 @@
+setPath($path);
+ $this->vars[self::GLOBAL_KEY] = array();
+ }
+
+ /**
+ * Присвоить переменную только
+ *
+ * @param string $name - имя переменной
+ * @param mixed $value - значение переменной
+ * @param string $template - шаблон, если не указан, то переменная глобальная
+ */
+ public function assign($name, $value, $template = self::GLOBAL_KEY)
+ {
+ $this->vars[$template][$name] = $value;
+ }
+
+ /**
+ * Очистить стек переменных
+ * @param boolean $with_local - включая локальные переменные
+ */
+ public function clear($template = self::GLOBAL_KEY)
+ {
+ $this->vars[$template] = array();
+ }
+
+ /**
+ * Обработать шаблон
+ *
+ * @param string $template - относительный путь до шаблона
+ * @return string - обработанное содержимое шаблона
+ */
+ public function fetch($template)
+ {
+ $this->template = $template; //дабы экстракт не перезатер нам переменную. Это важно! $this то он не перезатрет :)
+ extract($this->vars[self::GLOBAL_KEY]);
+ if (isset($this->vars[$this->template])) {
+ extract($this->vars[$this->template], EXTR_OVERWRITE);
+ }
+ ob_start();
+ if (!(include $this->path.'/'.$this->template) == 'OK') {
+ throw new MJException('Template '.$this->path.'/'.$this->template.' not found');
+ }
+ return ob_get_clean();
+ }
+
+ /**
+ * Установать путь до шаблонов
+ *
+ * @param string $path - путь до шаблонов
+ */
+ public function setPath($path)
+ {
+ $this->path = $path;
+ }
+
+//Функции для вызова из шаблонов.
+
+ protected function addHead($str)
+ {
+ $this->head_array[] = $str;
+ }
+
+ protected function getHead()
+ {
+ return $this->head_array;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/User.class.php b/User.class.php
new file mode 100644
index 0000000..3267274
--- /dev/null
+++ b/User.class.php
@@ -0,0 +1,90 @@
+password != md5($password)) {
+ return false;
+ }
+
+ self::setSession();
+ return true;
+ }
+
+ static function logout()
+ {
+ Env::setCookie(session_name(), '', 0);
+ Env::setCookie('login', '', 0);
+ Env::setCookie('login_hash', '', 0);
+ if (session_id()) {
+ session_destroy();
+ }
+ }
+
+ static function process()
+ {
+ if (Env::getCookie(session_name())) { //есть сессия
+ session_start();
+ } elseif (Env::getCookie('login') && Env::getCookie('login_hash')) {
+ self::remember();
+ }
+ }
+
+ static function setSession()
+ {
+ Env::setCookie('login', self::$user->login, TIME_NOW + LOGIN_COOKIE_TTL);
+ Env::setCookie('login_hash', self::getHash(), TIME_NOW + LOGIN_COOKIE_TTL);
+
+ session_start();
+
+ $_SESSION['user'] = self::$user;
+ }
+
+ static function remember()
+ {
+ if (! self::$user = self::getByLogin(Env::getCookie('login'))) {
+ self::logout();
+ }
+
+ if (Env::getCookie('login_hash') == self::getHash()) {
+ self::setSession();
+ } else {
+ self::logout();
+ }
+ }
+
+ static function getHash()
+ {
+ return md5(self::$user->id.'hckrz'.self::$user->login.'mst'.self::$user->password.'dai');
+ }
+
+ static function getInfo()
+ {
+ return Env::Session('user', self::$user);
+ }
+
+ static function isGuest()
+ {
+ return ! (bool) Env::Session('user');
+ }
+
+ static function getByLogin($login)
+ {
+ return Load::model('UserData')->getByLogin($login);
+ }
+}
+?>
\ No newline at end of file