From 68136a5270700377987fbc8f3d532f7d4a6ce1b2 Mon Sep 17 00:00:00 2001 From: Xeoncross Date: Sat, 12 Nov 2011 21:32:00 -0600 Subject: [PATCH] Complete system rewrite. Faster, lighter, better l10n/i18n support, removal of modules, full PSR-0 support, migrations system. It's a whole new animal. --- .gitignore | 18 +- Bootstrap.php | 86 +++++ CLI | 41 +++ Class/Controller/Index.php | 31 ++ Class/Controller/Page404.php | 19 + Class/Core/Cipher.php | 69 ++++ Class/Core/Controller.php | 58 +++ Class/Core/Cookie.php | 69 ++++ Class/Core/Database.php | 370 ++++++++++++++++++++ Class/Core/Directory.php | 82 +++++ Class/Core/Dispatch.php | 133 +++++++ Class/Core/Error.php | 126 +++++++ Class/Core/Form.php | 246 +++++++++++++ Class/Core/GD.php | 132 +++++++ Class/Core/HTML.php | 123 +++++++ Class/Core/Migration.php | 196 +++++++++++ Class/Core/Migration/MySQL.php | 182 ++++++++++ Class/Core/Migration/PGSQL.php | 193 ++++++++++ Class/Core/ORM.php | 623 +++++++++++++++++++++++++++++++++ Class/Core/ORM/APC.php | 43 +++ Class/Core/Pagination.php | 164 +++++++++ Class/Core/Service.php | 118 +++++++ Class/Core/Session.php | 86 +++++ Class/Core/Table.php | 144 ++++++++ Class/Core/Upload.php | 106 ++++++ Class/Core/Validation.php | 387 ++++++++++++++++++++ Class/Core/View.php | 68 ++++ Class/Core/XML.php | 58 +++ Class/Model/Permission.php | 25 ++ Class/Model/Resource.php | 22 ++ Class/Model/Role.php | 25 ++ Class/Model/User.php | 14 + Class/MyController.php | 80 +++++ Command/Backup.php | 25 ++ Command/Create.php | 25 ++ Command/Restore.php | 25 ++ Command/Run.php | 27 ++ Common.php | 544 ++++++++++++++++++++++++++++ Config/Sample.Config.php | 76 ++++ Config/Sample.Migration.php | 29 ++ Config/Sample.Route.php | 33 ++ Locale/.gitignore | 0 Public/index.php | 67 +--- README/install.txt | 14 +- README/sample.htaccess | 83 +---- README/sample.nginx.conf | 43 ++- View/404.php | 2 + View/Index/Index.php | 2 + View/Layout.php | 51 +++ View/Sidebar.php | 6 + View/System/Debug.php | 71 ++++ View/System/Error.php | 69 ++++ View/System/Exception.php | 73 ++++ 53 files changed, 5242 insertions(+), 160 deletions(-) create mode 100755 Bootstrap.php create mode 100755 CLI create mode 100755 Class/Controller/Index.php create mode 100755 Class/Controller/Page404.php create mode 100755 Class/Core/Cipher.php create mode 100755 Class/Core/Controller.php create mode 100755 Class/Core/Cookie.php create mode 100755 Class/Core/Database.php create mode 100755 Class/Core/Directory.php create mode 100644 Class/Core/Dispatch.php create mode 100755 Class/Core/Error.php create mode 100755 Class/Core/Form.php create mode 100755 Class/Core/GD.php create mode 100755 Class/Core/HTML.php create mode 100755 Class/Core/Migration.php create mode 100755 Class/Core/Migration/MySQL.php create mode 100755 Class/Core/Migration/PGSQL.php create mode 100755 Class/Core/ORM.php create mode 100755 Class/Core/ORM/APC.php create mode 100755 Class/Core/Pagination.php create mode 100755 Class/Core/Service.php create mode 100755 Class/Core/Session.php create mode 100644 Class/Core/Table.php create mode 100755 Class/Core/Upload.php create mode 100755 Class/Core/Validation.php create mode 100755 Class/Core/View.php create mode 100755 Class/Core/XML.php create mode 100755 Class/Model/Permission.php create mode 100755 Class/Model/Resource.php create mode 100755 Class/Model/Role.php create mode 100755 Class/Model/User.php create mode 100755 Class/MyController.php create mode 100755 Command/Backup.php create mode 100755 Command/Create.php create mode 100755 Command/Restore.php create mode 100755 Command/Run.php create mode 100755 Common.php create mode 100755 Config/Sample.Config.php create mode 100644 Config/Sample.Migration.php create mode 100644 Config/Sample.Route.php create mode 100644 Locale/.gitignore create mode 100755 View/404.php create mode 100755 View/Index/Index.php create mode 100644 View/Layout.php create mode 100755 View/Sidebar.php create mode 100755 View/System/Debug.php create mode 100755 View/System/Error.php create mode 100755 View/System/Exception.php diff --git a/.gitignore b/.gitignore index d780962..f185c72 100755 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,19 @@ +# Linux +.* +!.gitignore +*~ + +# OS X .DS_Store +# Thumbnails +._* + +# Windows Thumbs.db -*~ +Desktop.ini + +# MicroMVC *.cache *.log Uploads -config.php -ignore -Backups +Storage diff --git a/Bootstrap.php b/Bootstrap.php new file mode 100755 index 0000000..40116dc --- /dev/null +++ b/Bootstrap.php @@ -0,0 +1,86 @@ +events as $event => $class) +{ + event($event, NULL, $class); +} + +/* +if(preg_match_all('/[\-a-z]{2,}/i', getenv('HTTP_ACCEPT_LANGUAGE'), $locales)) +{ + $locales = $locales[0]; +} +*/ + +// Get locale from user agent +if(isset($_COOKIE['lang'])) +{ + $preference = $_COOKIE['lang']; +} +else +{ + $preference = Locale::acceptFromHttp(getenv('HTTP_ACCEPT_LANGUAGE')); +} + +// Match preferred language to those available, defaulting to generic English +$locale = Locale::lookup(config()->languages, $preference, false, 'en'); + +// Default Locale +Locale::setDefault($locale); +setlocale(LC_ALL, $locale . '.utf-8'); +//putenv("LC_ALL", $locale); + +// Default timezone of server +date_default_timezone_set('UTC'); + +// iconv encoding +iconv_set_encoding("internal_encoding", "UTF-8"); + +// multibyte encoding +mb_internal_encoding('UTF-8'); + +// Enable global error handling +set_error_handler(array('\Core\Error', 'handler')); +register_shutdown_function(array('\Core\Error', 'fatal')); + diff --git a/CLI b/CLI new file mode 100755 index 0000000..e8e0bb1 --- /dev/null +++ b/CLI @@ -0,0 +1,41 @@ +db = new DB(config('database')); + + // Set ORM database connection + //ORM::$db = $this->db; + + // Load the theme sidebar since we don't need the full page + $this->sidebar = new \Core\View('Sidebar'); + + // Load the welcome view + $this->content = new \Core\View('Index/Index'); + } +} diff --git a/Class/Controller/Page404.php b/Class/Controller/Page404.php new file mode 100755 index 0000000..e9dd0ef --- /dev/null +++ b/Class/Controller/Page404.php @@ -0,0 +1,19 @@ +show_404(); + } +} diff --git a/Class/Core/Cipher.php b/Class/Core/Cipher.php new file mode 100755 index 0000000..2fd023f --- /dev/null +++ b/Class/Core/Cipher.php @@ -0,0 +1,69 @@ +route = $route; + $this->dispatch = $dispatch; + } + + + /** + * Called before the controller method is run + * + * @param string $method name that will be run + */ + public function initialize($method) {} + + + /* HTTP Request Methods + abstract public function run(); // Default for all non-defined request methods + abstract public function get(); + abstract public function post(); + abstract public function put(); + abstract public function delete(); + abstract public function options(); + abstract public function head(); + */ + + /** + * Called after the controller method is run to send the response + */ + public function send() {} + +} + +// End diff --git a/Class/Core/Cookie.php b/Class/Core/Cookie.php new file mode 100755 index 0000000..5ba649f --- /dev/null +++ b/Class/Core/Cookie.php @@ -0,0 +1,69 @@ +cookie; + + if(isset($_COOKIE[$name])) + { + // Decrypt cookie using cookie key + if($v = json_decode(Cipher::decrypt(base64_decode($_COOKIE[$name]), $config['key']))) + { + // Has the cookie expired? + if($v[0] < $config['timeout']) + { + return is_scalar($v[1])?$v[1]:(array)$v[1]; + } + } + } + + return FALSE; + } + + + /** + * Called before any output is sent to create an encrypted cookie with the given value. + * + * @param string $key cookie name + * @param mixed $value to save + * @param array $config settings + * return boolean + */ + public static function set($name, $value, $config = NULL) + { + // Use default config settings if needed + extract($config ?: config()->cookie); + + // If the cookie is being removed we want it left blank + $value = $value ? base64_encode(Cipher::encrypt(json_encode(array(time(), $value)), $key)) : ''; + + // Save cookie to user agent + setcookie($name, $value, $expires, $path, $domain, $secure, $httponly); + } + +} + +// END diff --git a/Class/Core/Database.php b/Class/Core/Database.php new file mode 100755 index 0000000..ec5a492 --- /dev/null +++ b/Class/Core/Database.php @@ -0,0 +1,370 @@ +type = current(explode(':', $config['dns'], 2)); + + // Save config for connection + $this->config = $config; + + // MySQL uses a non-standard column identifier + if($this->type == 'mysql') $this->i = '`'; + } + + + /** + * Database lazy-loading to setup connection only when finally needed + */ + public function connect() + { + extract($this->config); + + // Clear config for security reasons + $this->config = NULL; + + // Connect to PDO + $this->pdo = new \PDO($dns, $username, $password, $params); + + // PDO should throw exceptions + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + + + /** + * Quotes a string for use in a query + * + * @param mixed $value to quote + * @return string + */ + public function quote($value) + { + if( ! $this->pdo) $this->connect(); + return $this->pdo->quote($value); + } + + + /** + * Run a SQL query and return a single column (i.e. COUNT(*) queries) + * + * @param string $sql query to run + * @param array $params the prepared query params + * @param int $column the optional column to return + * @return mixed + */ + public function column($sql, array $params = NULL, $column = 0) + { + // If the query succeeds, fetch the column + return ($statement = $this->query($sql, $params)) ? $statement->fetchColumn($column) : NULL; + } + + + /** + * Run a SQL query and return a single row object + * + * @param string $sql query to run + * @param array $params the prepared query params + * @param string $object the optional name of the class for this row + * @return array + */ + public function row($sql, array $params = NULL, $object = NULL) + { + if( ! $statement = $this->query($sql, $params)) return; + + $row = $statement->fetch(\PDO::FETCH_OBJ); + + // If they want the row returned as a custom object + if($object) $row = new $object($row); + + return $row; + } + + + /** + * Run a SQL query and return an array of row objects or an array + * consisting of all values of a single column. + * + * @param string $sql query to run + * @param array $params the optional prepared query params + * @param int $column the optional column to return + * @return array + */ + public function fetch($sql, array $params = NULL, $column = NULL) + { + if( ! $statement = $this->query($sql, $params)) return; + + // Return an array of records + if($column === NULL) return $statement->fetchAll(\PDO::FETCH_OBJ); + + // Fetch a certain column from all rows + return $statement->fetchAll(\PDO::FETCH_COLUMN , $column); + } + + + /** + * Run a SQL query and return the statement object + * + * @param string $sql query to run + * @param array $params the prepared query params + * @return PDOStatement + */ + public function query($sql, array $params = NULL, $cache_statement = FALSE) + { + $time = microtime(TRUE); + + self::$last_query = $sql; + + // Connect if needed + if( ! $this->pdo) $this->connect(); + + // Should we cached PDOStatements? (Best for batch inserts/updates) + if($cache_statement) + { + $hash = md5($sql); + + if(isset($this->statements[$hash])) + { + $statement = $this->statements[$hash]; + } + else + { + $statement = $this->statements[$hash] = $this->pdo->prepare($sql); + } + } + else + { + $statement = $this->pdo->prepare($sql); + } + + $statement->execute($params); + //$statement = $this->pdo->query($sql); + + // Save query results by database type + self::$queries[$this->type][] = array(microtime(TRUE) - $time, $sql); + + return $statement; + } + + + /** + * Run a DELETE SQL query and return the number of rows deleted + * + * @param string $sql query to run + * @param array $params the prepared query params + * @return int + */ + public function delete($sql, array $params = NULL) + { + if($statement = $this->query($sql, $params)) + { + return $statement->rowCount(); + } + } + + + /** + * Creates and runs an INSERT statement using the values provided + * + * @param string $table the table name + * @param array $data the column => value pairs + * @return int + */ + public function insert($table, array $data, $cache_statement = TRUE) + { + $sql = $this->insert_sql($table, $data); + + // PostgreSQL does not return the ID by default + if($this->type == 'pgsql') + { + // Insert record and return the whole row (the "id" field may not exist) + if($statement = $this->query($sql.' RETURNING "id"', array_values($data))) + { + // The first column *should* be the ID + return $statement->fetchColumn(0); + } + + return; + } + + // Insert data and return the new row's ID + return $this->query($sql, array_values($data), $cache_statement) ? $this->pdo->lastInsertId() : NULL; + } + + + /** + * Create insert SQL + * + * @param array $data row data + * @return string + */ + public function insert_sql($table, $data) + { + $i = $this->i; + + // Column names come from the array keys + $columns = implode("$i, $i", array_keys($data)); + + // Build prepared statement SQL + return "INSERT INTO $i$table$i ($i".$columns."$i) VALUES (" . rtrim(str_repeat('?, ', count($data)), ', ') . ')'; + } + + + /** + * Builds an UPDATE statement using the values provided. + * Create a basic WHERE section of a query using the format: + * array('column' => $value) or array("column = $value") + * + * @param string $table the table name + * @param array $data the column => value pairs + * @return int + */ + public function update($table, $data, array $where = NULL, $cache_statement = TRUE) + { + $i = $this->i; + + // Column names come from the array keys + $columns = implode("$i = ?, $i", array_keys($data)); + + // Build prepared statement SQL + $sql = "UPDATE $i$table$i SET $i" . $columns . "$i = ? WHERE "; + + // Process WHERE conditions + list($where, $params) = $this->where($where); + + // Append WHERE conditions to query and statement params + if($statement = $this->query($sql . $where, array_merge(array_values($data), $params), $cache_statement)) + { + return $statement->rowCount(); + } + } + + + /** + * Create a basic, single-table SQL query + * + * @param string $columns + * @param string $table + * @param array $where array of conditions + * @param int $limit + * @param int $offset + * @param array $order array of order by conditions + * @return array + */ + public function select($column, $table, $where = NULL, $limit = NULL, $offset = 0, $order = NULL) + { + $i = $this->i; + + $sql = "SELECT $column FROM $i$table$i"; + + // Process WHERE conditions + list($where, $params) = $this->where($where); + + // If there are any conditions, append them + if($where) $sql .= " WHERE $where"; + + // Append optional ORDER BY sorting + $sql .= self::order_by($order); + + if($limit) + { + // MySQL/SQLite use a different LIMIT syntax + $sql .= $this->type == 'pgsql' ? " LIMIT $limit OFFSET $offset" : " LIMIT $offset, $limit"; + } + + return array($sql, $params); + } + + + /** + * Generate the SQL WHERE clause options from an array + * + * @param array $where array of column => $value indexes + * @return array + */ + public function where($where = NULL) + { + $a = $s = array(); + + if($where) + { + $i = $this->i; + + foreach($where as $c => $v) + { + // Raw WHERE conditions are allowed array(0 => '"a" = NOW()') + if(is_int($c)) + { + $s[] = $v; + } + else + { + // Column => Value + $s[] = "$i$c$i = ?"; + $a[] = $v; + } + } + } + + // Return an array with the SQL string + params + return array(implode(' AND ', $s), $a); + } + + + /** + * Create the ORDER BY clause for MySQL and SQLite (still working on PostgreSQL) + * + * @param array $fields to order by + */ + public function order_by($fields = NULL) + { + if( ! $fields) return; + + $i = $this->i; + + $sql = ' ORDER BY '; + + // Add each order clause + foreach($fields as $k => $v) $sql .= "$i$k$i $v, "; + + // Remove ending ", " + return substr($sql, 0, -2); + } + +} + +// END diff --git a/Class/Core/Directory.php b/Class/Core/Directory.php new file mode 100755 index 0000000..f1fa4f9 --- /dev/null +++ b/Class/Core/Directory.php @@ -0,0 +1,82 @@ +$only()) $results[] = $file; + } + + return $results; + } + + + /** + * Make sure that a directory exists and is writable by the current PHP process. + * + * @param string $dir the directory to load + * @param string $chmod + * @return boolean + */ + static function usable($dir, $chmod = '0744') + { + // If it doesn't exist, and can't be made + if(! is_dir($dir) AND ! mkdir($dir, $chmod, TRUE)) return FALSE; + + // If it isn't writable, and can't be made writable + if(! is_writable($dir) AND !chmod($dir, $chmod)) return FALSE; + + return TRUE; + } + +} + +// END diff --git a/Class/Core/Dispatch.php b/Class/Core/Dispatch.php new file mode 100644 index 0000000..93da6b3 --- /dev/null +++ b/Class/Core/Dispatch.php @@ -0,0 +1,133 @@ +routes = $routes; + } + + public function controller($path, $method) + { + // Parse the routes to find the correct controller + list($params, $route, $controller) = $this->route($path); + + // Load and run action + $controller = new $controller($route, $this); + + // We are ignoring TRACE & CONNECT + $request_methods = array('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD'); + + // Look for a RESTful method, or try the default run() + if( ! in_array($method, $request_methods) OR ! method_exists($controller, $method)) + { + if( ! method_exists($controller, 'run')) + { + throw new \Exception('Invalid Request Method.'); + } + + $method = 'run'; + } + + // Controller setup here + $controller->initialize($method); + + if($params) + { + call_user_func_array(array($controller, $method), $params); + } + else + { + $controller->$method(); + } + + // Return the controller instance + return $controller; + } + + + /** + * Parse the given URL path and return the correct controller and parameters. + * + * @param string $path segment of URL + * @param array $routes to test against + * @return array + */ + public function route($path) + { + $path = trim($path, '/'); + + // Default homepage route + if($path === '') + { + return array(array(), '', $this->routes['']); + } + + // If this is not a valid, safe path (more complex params belong in GET/POST) + if($path AND ! preg_match('/^[\w\-~\/\.]{1,400}$/', $path)) + { + $path = '404'; + } + + foreach($this->routes as $route => $controller) + { + if( ! $route) continue; // Skip homepage route + + // Is this a regex? + if($route{0} === '/') + { + if(preg_match($route, $path, $matches)) + { + $complete = array_shift($matches); + + // The following code tries to solve: + // (Regex) "/^path/(\w+)/" + (Path) "path/word/other" = (Params) array(word, other) + + // Skip the regex match and continue from there + $params = explode('/', trim(mb_substr($path, mb_strlen($complete)), '/')); + + if($params[0]) + { + // Add captured group back into params + foreach($matches as $match) + { + array_unshift($params, $match); + } + } + else + { + $params = $matches; + } + + //print dump($params, $matches); + return array($params, $complete, $controller); + } + } + else + { + if(mb_substr($path, 0, mb_strlen($route)) === $route) + { + $params = explode('/', trim(mb_substr($path, mb_strlen($route)), '/')); + return array($params, $route, $controller); + } + } + } + + // Controller not found + return array(array($path), $path, $this->routes['404']); + } +} diff --git a/Class/Core/Error.php b/Class/Core/Error.php new file mode 100755 index 0000000..436c055 --- /dev/null +++ b/Class/Core/Error.php @@ -0,0 +1,126 @@ +error = $error; + $view->code = $code; + print $view; + + return TRUE; + } + + + public static function exception(\Exception $e) + { + self::$found = TRUE; + + // If the view fails, at least we can print this message! + $message = "{$e->getMessage()} [{$e->getFile()}] ({$e->getLine()})"; + + try + { + log_message($message); + self::header(); + + $view = new View('System/Exception'); + $view->exception = $e; + + print $view; + } + catch(\Exception $e) + { + print $message; + } + + exit(1); + } + + + /** + * Fetch and HTML highlight serveral lines of a file. + * + * @param string $file to open + * @param integer $number of line to highlight + * @param integer $padding of lines on both side + * @return string + */ + public static function source($file, $number, $padding = 5) + { + // Get lines from file + $lines = array_slice(file($file), $number-$padding-1, $padding*2+1, 1); + + $html = ''; + foreach($lines as $i => $line) + { + $html .= '' . sprintf('%' . mb_strlen($number + $padding) . 'd', $i + 1) . ' ' + . ($i + 1 == $number ? '' . h($line) . '' : h($line)); + } + return $html; + } + + + /** + * Fetch a backtrace of the code + * + * @param int $offset to start from + * @param int $limit of levels to collect + * @return array + */ + public static function backtrace($offset, $limit = 5) + { + $trace = array_slice(debug_backtrace(), $offset, $limit); + + foreach($trace as $i => &$v) + { + if( ! isset($v['file'])) + { + unset($trace[$i]); + continue; + } + $v['source'] = self::source($v['file'], $v['line']); + } + + return $trace; + } + +} + +// END diff --git a/Class/Core/Form.php b/Class/Core/Form.php new file mode 100755 index 0000000..c6983ff --- /dev/null +++ b/Class/Core/Form.php @@ -0,0 +1,246 @@ +validation = $validation; + $this->field = $field; + + if($field) + { + $this->attributes = array('name' => $this->field, 'id' => $this->field); + } + } + + + /** + * Add an array of attributes to the form element + * + * @param array $attributes to add + */ + public function attributes(array $attributes) + { + foreach($attributes as $key => $value) + { + $this->attributes[$key] = $value; + } + return $this; + } + + + /** + * Load a new instance of this object for the given field + */ + public function __get($field) + { + if(empty($this->fields[$field])) + { + $this->fields[$field] = new $this($this->validation, $field, $this->attributes); + } + return $this->fields[$field]; + } + + + /** + * Set the field value + * + * @param mixed $value + */ + public function value($value) + { + $this->value = $value; + return $this; + } + + + /** + * Add a form label before the given element + * + * @param string $label text + */ + public function label($label) + { + $this->label = $label; + return $this; + } + + + /** + * Set the form element to display as a selectbox + * + * @param array $options of select box + */ + public function select(array $options) + { + $this->type = 'select'; + $this->options = $options; + return $this; + } + + /** + * Set the form element to display as an input box + * + * @param string $type of input (text, password, hidden...) + */ + public function input($type) + { + $this->type = $type; + return $this; + } + + + /** + * Set the form element to display as a textarea + */ + public function textarea() + { + $this->type = 'textarea'; + return $this; + } + + + /** + * Wrap the given form element in this tag + * + * @param string $tag name + */ + public function wrap($tag) + { + $this->tag = $tag; + return $this; + } + + + /** + * Return the current HTML form as a string + */ + public function __toString() + { + try + { + if($this->field) + { + return $this->render_field(); + } + + if( ! $this->fields) return ''; + + $output = ''; + + foreach($this->fields as $field) $output .= $field; + + return $output; + } + catch(\Exception $e) + { + Error::exception($e); + return ''; + } + } + + + /** + * Render the given field + * + * @return string + */ + protected function render_field() + { + $html = "\n"; + + if( ! $this->attributes) + { + $this->attributes = array(); + } + + // Configure the attributes + $attributes = $this->attributes; + + // Get the current value + if($this->value !== NULL) + { + $value = $this->value; + } + else + { + $value = $this->validation->value($this->field); + } + + if($this->label) + { + $html .= '"; + } + + if($this->type == 'select') + { + $html .= \Core\HTML::select($this->field, $this->options, $value, $attributes); + } + elseif($this->type == 'textarea') + { + $html .= \Core\HTML::tag('textarea', $value, $attributes); + } + else + { + // Input field + $attributes = $attributes + array('type' => $this->type, 'value' => $value); + + $html .= \Core\HTML::tag('input', FALSE, $attributes); + } + + // If there was a validation error + if($error = $this->validation->error($this->field)) + { + if(isset($attributes['class'])) + { + $attributes['class'] .= ' error'; + } + else + { + $attributes['class'] = $this->field . ' ' . $this->type . ' error'; + } + + $html .= "\n
$error
"; + } + + if($this->tag) + { + $html = \Core\HTML::tag($this->tag, $html . "\n") . "\n"; + } + + return $html; + } + +} + +// END diff --git a/Class/Core/GD.php b/Class/Core/GD.php new file mode 100755 index 0000000..f21c56e --- /dev/null +++ b/Class/Core/GD.php @@ -0,0 +1,132 @@ + $x/$width) + { + $sy = $y/4-($height/4); + } + else + { + $sx = $x/2-($width/2); + } + } + + $new = imagecreatetruecolor($width, $height); + self::alpha($new); + + // Crop and resize image + imagecopyresampled($new, $image, 0, 0, $sx, $sy, $width, $height, $x-($x-($small*$width)), $y-($y-($small*$height))); + + return $new; + } + + + /** + * Preserve the alpha channel transparency in PNG images + * + * @param resource $image the image resource handle + */ + public static function alpha($image) + { + imagecolortransparent($image, imagecolorallocate($image, 0, 0, 0)); + imagealphablending($image, false); + imagesavealpha($image, true); + } + +} + +// END diff --git a/Class/Core/HTML.php b/Class/Core/HTML.php new file mode 100755 index 0000000..17c234d --- /dev/null +++ b/Class/Core/HTML.php @@ -0,0 +1,123 @@ + tag + * + * @param $email the users email address + * @param $size the size of the image + * @param $alt the alt text + * @param $type the default image type to show + * @param $rating max image rating allowed + * @return string + */ + public static function gravatar($email = '', $size = 80, $alt = 'Gravatar', $type = 'wavatar', $rating = 'g') + { + return '\"$alt\""; + } + + + /** + * Compiles an array of HTML attributes into an attribute string and + * HTML escape it to prevent malformed (but not malicious) data. + * + * @param array $attributes the tag's attribute list + * @return string + */ + public static function attributes(array $attributes = NULL) + { + if( ! $attributes) return; + + asort($attributes); + $h = ''; + foreach($attributes as $k => $v) + { + $h .= " $k=\"" . h($v) . '"'; + } + return $h; + } + + + /** + * Create an HTML tag + * + * @param string $tag the tag name + * @param string $text the text to insert between the tags + * @param array $attributes of additional tag settings + * @return string + */ + public static function tag($tag, $text = '', array $attributes = NULL) + { + return"\n<$tag" . self::attributes($attributes) . ($text === 0 ? ' />' : ">$text"); + } + + + /** + * Create an HTML Link + * + * @param string $url for the link + * @param string $text the link text + * @param array $attributes of additional tag settings + * @return string + */ + public static function link($url, $text = '', array $attributes = NULL) + { + if( ! $attributes) + { + $attributes = array(); + } + + return self::tag('a', $text, $attributes + array('href' => site_url($url))); + } + + + /** + * Auto creates a form select dropdown from the options given . + * + * @param string $name the select element name + * @param array $options the select options + * @param mixed $selected the selected options(s) + * @param array $attributes of additional tag settings + * @return string + */ + public static function select($name, array $options, $selected = NULL, array $attributes = NULL) + { + $h = ''; + foreach($options as $k => $v) + { + $a = array('value' => $k); + + // Is this element one of the selected options? + if($selected AND in_array($k, (array)$selected)) + { + $a['selected'] = 'selected'; + } + + $h .= self::tag('option', $v, $a); + } + + if( ! $attributes) + { + $attributes = array(); + } + + return self::tag('select', $h, $attributes+array('name' => $name)); + } + +} + +// END diff --git a/Class/Core/Migration.php b/Class/Core/Migration.php new file mode 100755 index 0000000..927343b --- /dev/null +++ b/Class/Core/Migration.php @@ -0,0 +1,196 @@ +tables) die('No tables given'); + + $file = $this->backup_path().$this->name.'_current_backup.json'; + + if(!is_file($file)) + { + // Report status to user + print 'Backup file not found ('. colorize($file, 'yellow').")\n"; + + return; + } + + $handle = fopen($file, "r"); + + //if(empty($tables)) die(colorize('Cannot restore backup, invalid JSON data', 'red')."\n"); + if( ! $handle) die(colorize('Cannot open backup file', 'red')."\n"); + + try + { + // Start transaction + $this->db->pdo->beginTransaction(); + + $table = NULL; + $columns = array(); + + /* + while (!feof($handle)) + { + $line = fread($handle, 8192); + */ + while (($line = fgets($handle)) !== false) + { + $line = rtrim($line); + + // Table name + if($line{0} !== '{') + { + $table = $line; + + // Has this table been removed from the schema? + if( ! isset($this->tables[$table]) ) + { + print colorize("$table no longer exists in schema, ignoring",'yellow')."\n"; + $table = NULL; + } + else + { + // Column list comes from new schema - not old backup + $columns = array_flip(array_keys($this->tables[$table])); + print colorize("Restoring $table...", 'green')."\n"; + } + + continue; + } + + if( ! $table) continue; // Current table is being ignored + + // Decode JSON row object + $line = (array) json_decode($line); + + /* + * Some databases (like PostgreSQL) cannot handle incorrect FALSE values. + * For example, PostgreSQL CANNOT handle empty ("") values in integer columns. + * + * So, we will simply remove all empty values from the insert giving + * them a default of NULL or EMPTY as the database decides. + */ + foreach($line as $key => $value) + { + //if( ! $value AND $value !== NULL) unset($line[$key]); + if($value === '') unset($line[$key]); + } + + // Insert row *only* taking schema columns into account + $this->db->insert($table, array_intersect_key((array) $line, $columns)); + } + + // Commit Transaction + $this->db->pdo->commit(); + + print colorize('Finished Restoring Data', 'blue'). "\n\n"; + } + catch(PDOException $e) + { + // Roolback changes (all or nothing) + $this->db->pdo->rollBack(); + + fclose($handle); + + die(colorize($e->getMessage(), 'red')."\n"); + } + + fclose($handle); + } + + // Path to backup files + protected function backup_path() + { + return SP . 'App/Backups/'; + } + + // Backup all existing data + protected function backup_tables($tables) + { + if( ! $this->tables) die('No tables given'); + + // Build path to backup directory + $file = $this->backup_path(). get_class($this). '.'. $this->name. '.'.date("Y.m.d_H:i").'.json'; + + // Open the backup file for writing and truncate it + $handle = fopen($file, 'w'); + + // Does anything actually get backed-up? + $found = FALSE; + + // Backup all data in this schema + foreach($this->tables as $table => $schema) + { + // Don't try to back it up if it doesn't exist + if( ! in_array($table, $tables)) + { + // Report status to user + print 'Skipping '. colorize($table, 'yellow')."\n"; + continue; + } + + $found = TRUE; + + // Start of new table + fwrite($handle, $table. "\n"); + + // Report status to user + print 'Backing up '. colorize($table, 'green')."\n"; + + // Fetch all records + $statement = $this->db->query('SELECT * FROM '. $this->db->i. $table. $this->db->i); + + // We want named keys + $statement->setFetchMode(\PDO::FETCH_ASSOC); + + // Write each record one at a time to save memory + foreach($statement as $row) + { + fwrite($handle, json_encode($row)."\n"); + } + } + + // We're done here + fclose($handle); + + if($found) + { + // Make this file the new masterbackup + copy($file, $this->backup_path() . $this->name . '_current_backup.json'); + + // Report status to user + print 'Backup saved to '. colorize($file, 'blue')."\n\n"; + } + else + { + print colorize('Nothing to backup', 'yellow')."\n"; + } + } +} diff --git a/Class/Core/Migration/MySQL.php b/Class/Core/Migration/MySQL.php new file mode 100755 index 0000000..c4ab811 --- /dev/null +++ b/Class/Core/Migration/MySQL.php @@ -0,0 +1,182 @@ +tables) die('No tables given'); + + $tables = array(); + + // Build list of all tables + foreach($this->db->fetch('SHOW TABLES') as $row) $tables[] = current($row); + + if($tables) + { + $this->backup_tables($tables); + } + } + + // Drop database schema + public function drop_schema() + { + // + } + + /** + * Create database schema + */ + public function create_schema() + { + if( ! $this->tables) die('No tables given'); + + // First force the schema to use UTF-8 + //$this->db->query("ALTER DATABASE `micromvc` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci"); + + // Create each table + foreach($this->tables as $table => $schema) + { + // Report status to user + print 'Dropping table '. colorize($table, 'green')."\n"; + + // Remove table + $this->db->query("DROP TABLE IF EXISTS `$table`"); + + $sql = "CREATE TABLE `$table` (\n"; + + $index = array(); + $unique = array(); + $primary = NULL; + + // Defaults for columns + $defaults = array( + //'type' => 'primary|string|integer|boolean|decimal|datetime', REQUIRED! + 'type' => 'string', + 'length' => NULL, + 'index' => FALSE, + 'null' => TRUE, + 'default' => NULL, + 'unique' => FALSE, + 'precision' => 0, + 'scale' => 0, + ); + + foreach($schema as $column => $data) + { + $data = $data + $defaults; + + $type = $data['type']; + + // Integer? + if($type == 'primary' OR $type == 'integer') + { + // Default to int + $length = $data['length'] ? $data['length'] : 2147483647; + + if($length <= 127) + $type = 'TINYINT'; + elseif($length <= 32767) + $type = 'SMALLINT'; + elseif($length <= 8388607) + $type = 'MEDIUMINT'; + elseif($length <= 2147483647) + $type = 'INT'; + else + $type = 'BIGINT'; + + // Is this the primary column? + if($data['type'] == 'primary') + { + $primary = $column; + + // Primary keys are special + $sql .= "\t`$column` $type unsigned NOT NULL AUTO_INCREMENT,\n"; + continue; + } + } + elseif($type == 'string') + { + // Default to text + $length = $data['length'] ? $data['length'] : 65535; + + if($length <= 255) + $type = 'VARCHAR('. $length.')'; + elseif($length <= 65535) + $type = 'TEXT'; + elseif($length <= 16777215) + $type = 'MEDIUMTEXT'; + else + $type = 'LONGTEXT'; + } + elseif($type == 'boolean') + { + $type = 'TINYINT(1)'; + } + elseif($type == 'decimal') + { + $type = 'DECIMAL('. $data['precision'].','. $data['scale'].')'; + } + else + { + $type = 'DATETIME'; + } + + // Build Column Definition + $sql .= "\t`$column` $type"; + + if(! $data['null']) $sql .= ' NOT NULL'; + + if($data['default']) $sql .= ' DEFAULT \''. $data['default']. "'"; + + $sql .= ",\n"; + + // Is the column unique? + if($data['unique']) $unique[] = $column; + + // Index the column? + if($data['index']) $index[] = $column; + } + + if($primary) $sql .= "PRIMARY KEY (`$primary`),\n"; + + foreach($unique as $column) + { + $sql .= "UNIQUE KEY `$column` (`$column`),\n"; + } + + foreach($index as $column) + { + $sql .= "KEY `$column` (`$column`),\n"; + } + + // Remove ending comma + $sql = substr($sql,0,-2)."\n"; + + $sql .=') ENGINE = InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci'; + //$sql .=') ENGINE = INNODB CHARACTER SET utf8 COLLATE utf8_general_ci'; + + // Create table + $this->db->query($sql); + + // Report status to user + print 'Created table '. colorize($table, 'green')."\n"; + } + + print colorize('Schema Created', 'blue')."\n\n"; + } + +} diff --git a/Class/Core/Migration/PGSQL.php b/Class/Core/Migration/PGSQL.php new file mode 100755 index 0000000..6179ee4 --- /dev/null +++ b/Class/Core/Migration/PGSQL.php @@ -0,0 +1,193 @@ +tables) die('No tables given'); + + $tables = array(); + + $sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"; + + // Build list of all tables + foreach($this->db->fetch($sql) as $row) $tables[] = current($row); + + if($tables) + { + $this->backup_tables($tables); + } + } + + // Drop database schema + public function drop_schema() + { + // + } + + /** + * Create database schema + */ + public function create_schema() + { + if( ! $this->tables) die('No tables given'); + + // Create each table + foreach($this->tables as $table => $schema) + { + // Report status to user + print 'Dropping table '. colorize($table, 'yellow')."\n"; + + // Remove table + $this->db->query("DROP TABLE IF EXISTS \"$table\""); + + $sql = "CREATE TABLE \"$table\" (\n"; + + $index = array(); + $unique = array(); + + // Defaults for columns + $defaults = array( + //'type' => 'primary|string|integer|boolean|decimal|datetime', REQUIRED! + 'type' => 'string', + 'length' => NULL, + 'index' => FALSE, + 'null' => TRUE, + 'default' => '', + 'unique' => FALSE, + 'precision' => 0, + 'scale' => 0, + ); + + foreach($schema as $column => $data) + { + $data = $data + $defaults; + + $type = $data['type']; + + // Integer? + if($type == 'primary' OR $type == 'integer') + { + // Default to int + $length = $data['length'] ? $data['length'] : 2147483647; + + if($length <= 32767) + $type = 'smallint'; + elseif($length <= 2147483647) + $type = 'integer'; + else + $type = 'bigint'; + + // Is this the primary column? + if($data['type'] == 'primary') + { + $primary = $column; + + // Primary keys are special + $sql .= "\t\"$column\" serial primary key,\n"; + continue; + } + } + elseif($type == 'string') + { + // Even if "text" isn't a valid type in SQL + // PostgreSQL treats it the same as "character varying" (i.e. "varchar") + $type = 'text'; + } + elseif($type == 'boolean') + { + $type = 'boolean'; + } + elseif($type == 'decimal') + { + $type = 'decimal('. $data['precision'].','. $data['scale'].')'; + } + else + { + $type = 'timestamp without time zone'; + } + + // Build Column Definition + $sql .= "\t\"$column\" $type"; + + // NULL and FALSE are both valid defaults + if($data['default'] !== '') + { + if(is_bool($data['default']) OR $data['default'] === NULL) + { + $sql .= ' DEFAULT '. $data['default']; + } + else + { + $sql .= ' DEFAULT \''. $data['default']. "'"; + } + } + + // Add NULL + if(! $data['null']) $sql .= ' NOT NULL'; + + $sql .= ",\n"; + + // Is the column unique? + if($data['unique']) $unique[] = $column; + + // Index the column? + if($data['index']) $index[] = $column; + } + + foreach($unique as $column) + { + $key = $table.'_'. $column.'_u';//.chr(mt_rand(65,90)); + + $sql .= "CONSTRAINT $key UNIQUE ($column),\n"; + + // Creating a unique constraint automattically creates an index + foreach($index as $id => $field) + { + if($field === $column) + { + unset($index[$id]); + } + } + } + + // Remove ending comma and close table + $sql = substr($sql,0,-2)."\n);"; + + // Create table + print $sql."\n"; + $this->db->query($sql); + + // Create any indexes + foreach($index as $column) + { + $key = $table.'_'. $column.'_i';//.chr(mt_rand(65,90)); + $sql = "CREATE INDEX $key ON \"$table\" USING btree ($column)"; + + print $sql."\n"; + $this->db->query($sql); + } + + // Report status to user + print 'Created table '. colorize($table, 'green')."\n\n"; + + } + + print colorize('Schema Created', 'blue')."\n\n"; + } + +} diff --git a/Class/Core/ORM.php b/Class/Core/ORM.php new file mode 100755 index 0000000..b07c205 --- /dev/null +++ b/Class/Core/ORM.php @@ -0,0 +1,623 @@ +data = array(); + + if(! $id) return; + + if(is_numeric($id)) + { + $this->data[static::$key] = $id; + } + else + { + $this->data = (array) $id; + $this->loaded = 1; + } + + $this->saved = 1; + } + + + /** + * Get this object's primary key + * + * @return int + */ + public function key() + { + return isset($this->data[static::$key])?$this->data[static::$key]:NULL; + } + + + /** + * Return object data as array + * + * @return array + */ + public function to_array() + { + if($this->load()) return $this->data; + } + + + /** + * Set an array of values on this object + * + * @param array $values to set + * @return object + */ + public function set($values) + { + foreach($values as $key => $value) + { + $this->__set($key, $value); + } + return $this; + } + + + /** + * Set a propery of this object + * + * @param string $key name + * @param mixed $v value + */ + public function __set($key, $value) + { + if( ! array_key_exists($key, $this->data) OR $this->data[$key] !== $value) + { + $this->data[$key] = $value; + $this->changed[$key] = $key; + $this->saved = 0; + } + } + + + /** + * Retive a property or 1-to-1 object relation + * + * @param string $key the column or relation name + * @return mixed + */ + public function __get($key) + { + // All this to get the primary key without loading the entity + if(isset($this->data[static::$key])) + { + if($key == static::$key) return $this->data[static::$key]; + if( ! $this->loaded) $this->load(); + } + + //if(isset($this->data[static::$key]) AND ! $this->loaded) $this->load(); + return array_key_exists($key, $this->data) ? $this->data[$key] : $this->related($key); + } + + + /** + * @see isset() + */ + public function __isset($key) + { + if(isset($this->data[static::$key]) AND ! $this->loaded) $this->load(); + return array_key_exists($key, $this->data) OR isset($this->related[$key]); + } + + + /** + * @see unset() + */ + public function __unset($key) + { + unset($this->data[$key], $this->changed[$key], $this->related[$key]); + } + + + /** + * Reload the current object from the database + * + * @return boolean + */ + public function reload() + { + $key = $this->key(); + $this->data = $this->changed = $this->related = array(); + $this->loaded = FALSE; + if(! $key) return; + $this->data[static::$key] = $key; + return $this->load(); + } + + + /** + * Clear the current object + */ + public function clear() + { + $this->data = $this->changed = $this->related = array(); + $this->loaded = $this->saved = FALSE; + } + + + /** + * Attempt to load the object record from the database + * + * @return boolean + */ + public function load(array $where = NULL) + { + $key = static::$key; + + if($where) + { + // Find the record primary key in the database + $id = self::select('column', static::$key, NULL, $where); + + if(empty($id)) + { + $this->clear(); + return FALSE; + } + + $this->data[$key] = $id; + } + else + { + // Did we already load this object? + if($this->loaded) return TRUE; + + if(empty($this->data[$key])) + { + //$this->clear(); + return FALSE; + } + + // Use the record primary key given in constructor + $id = $this->data[$key]; + } + + + // First check the cache + if(!($row = static::cache_get(static::$table . $id))) + { + // Then get from the database and cache + if($row = self::select('row', '*', $this, array($key => $id))) + { + static::cache_set(static::$table . $id, $row); + } + } + + if($row) + { + $this->data = (array) $row; + return $this->saved = $this->loaded = TRUE; + } + else + { + $this->clear(); + } + } + + + /** + * Load a related 1-to-1 object + * + * @param string $alias relation alias + * @return object + */ + public function related($alias) + { + // Already loaded? + if(isset($this->related[$alias])) return $this->related[$alias]; + + if(isset(static::$belongs_to[$alias])) + { + $model = static::$belongs_to[$alias]; + + if(is_array($model)) + { + $foreign_key = key($model); + $model = current($model); + } + else + { + $foreign_key = $model::$foreign_key; + } + + return $this->related[$alias] = new $model($this->data[$foreign_key]); + } + elseif(isset(static::$has[$alias])) + { + $model = static::$has[$alias]; + + if(is_array($model)) + { + $foreign_key = key($model); + $model = current($model); + } + else + { + $foreign_key = static::$foreign_key; + } + + // Fetch the ID of the models row + $id = self::select('column', $model::$key, $model, array($foreign_key => $this->key())); + + return $this->related[$alias] = new $model($id); + } + else + { + throw new \Exception(get_class($this). " propery $alias not found"); + } + } + + + /** + * Load a has_many relation set from another model using the filtering options of fetch() + * + * @param string $m alias name + * @param mixed $a arguments to pass + * @return array + */ + public function __call($alias, $args) + { + $method = 'fetch'; + + if(substr($alias, 0, 6) === 'count_') + { + $method = 'count'; + $alias = substr($alias, 6); + } + + // Append the default filter options + $args = $args + array(array(), 0, 0, array()); + + + // Is this a has one/many relation? + if(isset(static::$has[$alias])) + { + $model = static::$has[$alias]; + + if(is_array($model)) + { + $foreign_key = key($model); + $model = current($model); + } + else + { + $foreign_key = static::$foreign_key; + } + + // Set the foreign key WHERE condition + $args[0][$foreign_key] = $this->key(); + + return $model::$method($args[0], $args[1], $args[2], $args[3]); + } + + if(empty(static::$has_many_through[$alias])) + { + throw new Exception ($alias . ' relation not found'); + } + + $model = static::$has_many_through[$alias]; + + $foreign_key = key($model); + $model = current($model); + + next($model); + + $foreign_key_2 = key($model); + $model_2 = current($model); + + // Set the foreign key WHERE condition + $where = array($foreign_key => $this->key()) + $args[0]; + + // Fetch an array of objects by the foreign key so we can load from memory + return self::objects($foreign_key_2, $model_2, $model, $where, $args[1], $args[2], $args[3]); + } + + + /** + * Load an array of objects from the database + * + * @param string $column column to load + * @param object $class class to load into + * @param object $model model to search + * @param array $where where conditions + * @param int $limit limit + * @param int $offset offset + * @param array $order by conditions + * @return array + */ + public static function objects($column = NULL, $class = NULL, $model = NULL, $where = NULL, $limit = 0, $offset = 0, $order = NULL) + { + if($rows = self::select('fetch', $column, $model, $where, $limit, $offset, $order)) + { + $class = $class ?: get_called_class(); + foreach($rows as $id => $row) + { + $rows[$id] = new $class($row); + } + } + return $rows; + } + + + /** + * Load a SELECT query result set + * + * @param string $func function name (column/row/fetch) + * @param string $column column(s) to fetch + * @param object $model model to search + * @param array $where where conditions + * @param int $limit limit + * @param int $offset + * @param array $order by conditions + * @return mixed + */ + public static function select($func, $column, $model = NULL, $where = NULL, $limit = 0, $offset = 0, $order = NULL) + { + $model = $model ?: get_called_class(); + $order = ($order ?: array()) + (static::$order_by ?: array()); + + // Count queries don't have offsets, limits, or order conditions + if($func != 'fetch') + { + $limit = $offset = 0; + $order = array(); + } + + // Generate select statement SQL + list($sql, $params) = static::$db->select(($column ? $column : 'COUNT(*)'), $model::$table, $where, $limit, $offset, $order); + + return static::$db->$func($sql, $params, ($column == '*' ? NULL : 0)); + } + + + /** + * Fetch an array of objects from this table + * + * @param array $where conditions + * @param int $limit filter + * @param int $offset filter + * @param array $order_by conditions + */ + public static function fetch(array $where = NULL, $limit = 0, $offset = 0, array $order_by = NULL) + { + return self::objects(static::$key, 0, 0, $where, $limit, $offset, $order_by); + } + + + /** + * Count all database rows matching the conditions + * + * @param array $where conditions + * @return int + */ + public static function count(array $where = NULL) + { + return self::select('column', NULL, NULL, $where); + } + + + /** + * Return the result column of the row that matches the where condition. + * This can be used to get a rows primary key. + * + * @param array $where conditions + * @return int + */ + public static function column(array $where = NULL, $column = NULL) + { + return self::select('column', $column ? $column : static::$key, NULL, $where); + } + + + /** + * Return the ORM object which matches the where condition + * + * @param array $where conditions + * @return int + */ + public static function row(array $where = NULL) + { + if($id = self::select('column', static::$key, NULL, $where)) + { + $class = get_called_class(); + return new $class($id); + } + } + + + /** + * Save the current object to the database + */ + public function save() + { + if( ! $this->changed) return $this; + + $data = array(); + foreach($this->changed as $column) + { + $data[$column] = $this->data[$column]; + } + + if(isset($this->data[static::$key])) + { + $this->update($data); + } + else + { + $this->insert($data); + } + + $this->changed = array(); + return $this; + } + + + /** + * Insert the current object into the database table + * + * @param array $data to insert + * @return int + */ + protected function insert(array $data) + { + $id = static::$db->insert(static::$table, $data); + + $this->data[static::$key] = $id; + $this->loaded = $this->saved = 1; + return $id; + } + + + /** + * Update the current object in the database table + * + * @param array $d data + * @return boolean + */ + protected function update(array $data) + { + $result = static::$db->update(static::$table, $data, array(static::$key => $this->data[static::$key])); + + // Invalidate cache + static::cache_delete(static::$table . $this->data[static::$key]); + + $this->saved = 1; + return $result; + } + + + /** + * Delete the current object (and all related objects) from the database + * + * @param int $id to delete + * @return int + */ + public function delete($id = NULL) + { + $id = $id ?: $this->key(); + + $count = 0; + + // Remove all related entities too? + if(static::$cascade_delete) + { + $count = $this->delete_relations(); + } + + $table = static::$db->i . static::$table . static::$db->i; + + // Then remove this entity + $count += static::$db->delete('DELETE FROM ' . $table . ' WHERE ' . static::$key . ' = ?', array($id)); + + // Remove remaining traces + static::cache_delete(static::$table . $id); + $this->clear(); + + return $count; + } + + + /** + * Delete all the related objects that belong to the current object + * + * @return int + */ + public function delete_relations() + { + $count = 0; + foreach(static::$has as $alias => $model) + { + foreach($this->$alias() as $object) + { + // This object may also have entities to remove first + $count += $object->delete(); + } + } + return $count; + } + + + /** + * Store a value in the cache + * + * @param string $key name + * @param mixed $value to store + */ + public static function cache_set($key, $value){} + + + /** + * Fetch a value from the cache + * + * @param string $key name + * @return mixed + */ + public static function cache_get($key){} + + + /** + * Delete a value from the cache + * + * @param string $key name + * @return boolean + */ + public static function cache_delete($key){} + + + /** + * Check that a value exists in the cache + * + * @param string $key name + * @return boolean + */ + public static function cache_exists($key){} + +} + +// END diff --git a/Class/Core/ORM/APC.php b/Class/Core/ORM/APC.php new file mode 100755 index 0000000..058fe50 --- /dev/null +++ b/Class/Core/ORM/APC.php @@ -0,0 +1,43 @@ + 'pagination', 'id' => 'pagination'); + + + /** + * Creates pagination links for the total number of pages + * + * @param int $total number of items + * @param int $current page + * @param int $per_page the number to show per-page (default 10) + * @param string $path to place in the links + * @return string + */ + public function __construct($total, $current, $per_page = 10, $path = NULL, $params = NULL) + { + $this->current = (int) $current; + $this->per_page = (int) $per_page; + $this->total = (int) ceil($total / $per_page); + $this->path = $path; + + // Assume the current URL parameters if not given + if($params === NULL) + { + $this->params = $_GET; + } + elseif($params) + { + $this->params = $params; + } + } + + + /** + * Create a "previous page" link if needed + * + * @return string + */ + public function previous() + { + if($this->current > 1) + { + return HTML::tag('li', HTML::link($this->url($this->current-1), _('← Previous'), array('class' => 'previous'))); + } + } + + + /** + * Create a "first page" link if needed + * + * @return string + */ + public function first() + { + if($this->current > $this->links + 1) + { + return HTML::tag('li', HTML::link($this->url(1), _('< First')), array('class' => 'first')); + } + } + + + /** + * Create a "last page" link if needed + * + * @return string + */ + public function last() + { + if($this->current + $this->links < $this->total) + { + return HTML::tag('li', HTML::link($this->url($this->total), _('Last >')), array('class' => 'last')); + } + } + + + /** + * Create a "next page" link if needed + * + * @return string + */ + public function next() + { + $attributes = array('class' => 'next'); + + if($this->total < 2 OR $this->current < $this->total) + { + $attributes = array('class' => 'disabled next'); + } + + return HTML::tag('li', HTML::link($this->url($this->current+1), _('Next →')), $attributes); + } + + + + /** + * Return an HTML pagination string + * + * @return string + */ + public function __toString() + { + try + { + // Start and end must be valid integers + $start = (($this->current - $this->links) > 0) ? $this->current - $this->links : 1; + $end = (($this->current + $this->links) < $this->total) ? $this->current + $this->links : $this->total; + + $html = $this->previous(); + + for($i = $start; $i <= $end; ++$i) + { + // Current link is "active" + $attributes = $this->current == $i ? array('class' => 'active') : array(); + + // Wrap the link in a list item + $html .= HTML::tag('li', HTML::link($this->url($i), $i), $attributes); + } + + $html .= $this->next(); + + return HTML::tag('div', "\n", $this->attributes); + } + catch(\Exception $e) + { + Error::exception($e); + return ''; + } + + } + + + /** + * Build the pagination URL + * + * @param integer $page number + */ + public function url($page = NULL) + { + return site_url($this->path, (array) $this->params + array('page' => $page)); + } +} + +// END diff --git a/Class/Core/Service.php b/Class/Core/Service.php new file mode 100755 index 0000000..60df31b --- /dev/null +++ b/Class/Core/Service.php @@ -0,0 +1,118 @@ +db = function($service, $config) + * { + * return new DB($config); + * } + * + * // Create Database object + * $service->db(config('database')); + * + * // Use newly created database object + * $service->db()->query('SELECT * FROM table'); + * + * Another example is classes which have dependencies on other classes. + * + * // Create Error Object Instructions + * $service->error = function($service) + * { + * return new Error($service->log()); + * }; + * + * // Create Log Object Instructions + * $service->log = function($service) + * { + * return new File_Log(); + * }; + * + * // Creates Error and File_Log classes, return Error instance, then report this error + * $service->error->report('This will be logged'); + * + * @package MicroMVC + * @author David Pennington + * @copyright (c) 2011 MicroMVC Framework + * @license http://micromvc.com/license + ********************************** 80 Columns ********************************* + */ +namespace Core; + +class Service +{ + + protected $s = array(); + + /** + * Set an object or closure + * + * @param string $key name + * @param mixed $callable closure or object instance + */ + function __set($key, $callable) + { + // Like normal PHP, property/method names should be case-insensitive. + $key = strtolower($key); + + // Simple object storage? + if( ! $callable instanceof \Closure) + { + $this->s[$key] = $callable; + return; + } + + // Create singleton wrapper function tied to this service object + $this->s[$key] = function ($c, array $arg) use ($callable) + { + static $object; + if (is_null($object)) + { + array_unshift($arg, $c); + $object = call_user_func_array($callable, $arg); + } + return $object; + }; + } + + + /** + * Fetch an object or closure + * + * @param string $key name + * @return mixed + */ + function __get($key) + { + return $this->s[$key]; + } + + /** + * Check that the given key name exists + * + * @param string $key name + * @return mixed + */ + function __isset($key) + { + return isset($this->s[$key]); + } + + + /** + * Call the given closure singleton function + * + * @param string $key name + * @param array $arg for closure + * @return mixed + */ + function __call($key, $arg) + { + return $this->s[$key]($this, $arg); + } + +} diff --git a/Class/Core/Session.php b/Class/Core/Session.php new file mode 100755 index 0000000..0f79506 --- /dev/null +++ b/Class/Core/Session.php @@ -0,0 +1,86 @@ +" name = "token" /> + * + * @package MicroMVC + * @author David Pennington + * @copyright (c) 2011 MicroMVC Framework + * @license http://micromvc.com/license + ********************************** 80 Columns ********************************* + */ +namespace Core; + +class Session +{ + + /** + * Configure the session settings, check for problems, and then start the session . + * + * @param array $config an optional configuration array + * @return boolean + */ + public static function start($name = 'session') + { + // Was the session already started? + if( ! empty($_SESSION)) return FALSE; + $_SESSION = Cookie::get($name); + return TRUE; + } + + + /** + * Called at end-of-page to save the current session data to the session cookie + * + * return boolean + */ + public static function save($name = 'session') + { + return Cookie::set($name, $_SESSION); + } + + + /** + * Destroy the current users session + */ + public static function destroy($name = 'session') + { + Cookie::set($name, ''); + unset($_COOKIE[$name], $_SESSION); + } + + + /** + * Create new session token or validate the token passed + * + * @param string $token value to validate + * @return string|boolean + */ + public static function token($token = NULL) + { + if( ! isset($_SESSION)) return FALSE; + + // If a token is given, then lets match it + if($token !== NULL) + { + if( ! empty($_SESSION['token']) && $token === $_SESSION['token']) + { + return TRUE; + } + + return FALSE; + } + + return $_SESSION['token'] = token(); + } + +} + +// END diff --git a/Class/Core/Table.php b/Class/Core/Table.php new file mode 100644 index 0000000..3a5e81d --- /dev/null +++ b/Class/Core/Table.php @@ -0,0 +1,144 @@ +rows = $rows; + + // Set order defaults + $this->params = $_GET; + $this->column = get('column'); + $this->sort = get('sort', 'asc'); + $this->attributes = array('class' => 'table'); + } + + + /** + * Add a new field to the validation object + * + * @param string $field name + */ + public function column($header, $name, $function = NULL) + { + $this->columns[$header] = array($name, $function); + + return $this; + } + + + public function render() + { + $html = "\n\t\n\t\t"; + + foreach($this->columns as $header => $data) + { + $html .= "\n\t\t\t"; + + // If we allow sorting by this column + if($data[0]) + { + // If this column matches the current sort column - go in reverse + if($this->column === $data[0]) + { + $sort = $this->sort == 'asc' ? 'desc' : 'asc'; + } + else + { + $sort = $this->sort == 'asc' ? 'asc' : 'desc'; + } + + // Build URL parameters taking existing parameters into account + $url = site_url(NULL, array('column' => $data[0], 'sort' => $sort) + $this->params); + + $html .= '' . $header . ''; + } + else + { + $html .= $header; + } + + $html .= ""; + } + + $html .= "\n\t\t\n\t\n\t"; + + $odd = 0; + foreach($this->rows as $row) + { + $odd = 1 - $odd; + + $html .= "\n\t\t'; + foreach($this->columns as $header => $data) + { + if($data[1]) + { + $html .= "\n\t\t\t" . $data[1]($row) . ""; + } + else + { + $html .= "\n\t\t\t" . $row->$data[0] . ""; + } + } + $html .= "\n\t\t"; + } + + $html .= "\n\t\n"; + + return HTML::tag('table', $html, $this->attributes); + } + + + /** + * alias for render() + * + * @return string + */ + public function __toString() + { + try + { + return $this->render(); + } + catch(\Exception $e) + { + Error::exception($e); + return ''; + } + } +} + +// END diff --git a/Class/Core/Upload.php b/Class/Core/Upload.php new file mode 100755 index 0000000..72434d4 --- /dev/null +++ b/Class/Core/Upload.php @@ -0,0 +1,106 @@ + $file['size']) + { + return FALSE; + } + + // Create $basename, $filename, $dirname, & $extension variables + extract(pathinfo($file['name']) + array('extension' => '')); + + // Make the name file system safe + $filename = sanitize_filename($filename); + + // We must have a valid name and file type + if(empty($filename) OR empty($extension)) return FALSE; + + $extension = strtolower($extension); + + // Don't allow just any file! + if( ! $this->allowed_file($extension)) return FALSE; + + // Make sure we can use the destination directory + Directory::usable($dir); + + // Create a unique name if we don't want files overwritten + $name = $overwrite ? "$filename.$ext" : $this->unique_filename($dir, $filename, $extension); + + // Move the file to the correct location + if(move_uploaded_file($file['tmp_name'], $dir . $name)) + { + return $name; + } + } + + + /** + * Is the file extension allowed + * + * @param string $ext of the file + * @return boolean + */ + public function allowed_file($ext) + { + if( ! $this->allowed_files) return TRUE; + return in_array($ext, explode('|', $this->allowed_files)); + } + + + /** + * Create a unique filename by appending a number to the end of the file + * + * @param string $dir to check + * @param string $file name to check + * @param string $ext of the file + * @return string + */ + public function unique_filename($dir, $file, $ext) + { + // We start at null so a number isn't added unless needed + $x = NULL; + while(file_exists("$dir$file$x.$ext")) + { + $x++; + } + return"$file$x.$ext"; + } + +} + +// END diff --git a/Class/Core/Validation.php b/Class/Core/Validation.php new file mode 100755 index 0000000..3396462 --- /dev/null +++ b/Class/Core/Validation.php @@ -0,0 +1,387 @@ +'; + + // The text to put after an error + public $error_suffix = ''; + + /** + * Create the validation object using this data + * + * @param array $data to validate + */ + public function __construct($data) + { + $this->data = $data; + } + + + /** + * Add a new field to the validation object + * + * @param string $field name + */ + public function field($field) + { + $this->field = $field; + return $this; + } + + + /** + * Return the value of the given field + * + * @param string $field name to use instead of current field + * @return mixed + */ + public function value($field = NULL) + { + if( ! $field) + { + $field = $this->field; + } + + if(isset($this->data[$field])) + { + return $this->data[$field]; + } + } + + + /** + * Return success if validation passes! + * + * @return boolean + */ + public function validates() + { + return ! $this->errors; + } + + + /** + * Fetch validation error for the given field + * + * @param string $field name to use instead of current field + * @param boolean $wrap error with suffix/prefix + * @return string + */ + public function error($field = NULL, $wrap = FALSE) + { + if( ! $field) + { + $field = $this->field; + } + + if(isset($this->errors[$field])) + { + if($wrap) + { + return $this->error_prefix . $this->errors[$field] . $this->error_suffix; + } + + return $this->errors[$field]; + } + } + + + /** + * Return all validation errors as an array + * + * @return array + */ + public function errors() + { + return $this->errors; + } + + + /** + * Return all validation errors wrapped in HTML suffix/prefix + * + * @return string + */ + public function __toString() + { + $output = ''; + foreach($this->errors as $error) + { + $output .= $this->error_prefix . $error . $this->error_suffix . "\n"; + } + return $output; + } + + + /** + * Middle-man to all rule functions to set the correct error on failure. + * + * @param string $rule + * @param array $args + * @return this + */ + public function __call($rule, $args) + { + if(isset($this->errors[$this->field]) OR empty($this->data[$this->field])) return $this; + + // Add method suffix + $method = $rule . '_rule'; + + // Defaults for $error, $params + $args = $args + array(NULL, NULL); + + // If the validation fails + if( ! $this->$method($this->data[$this->field], $args[1])) + { + $this->errors[$this->field] = $args[0]; + } + + return $this; + } + + + /** + * Value is required and cannot be empty. + * + * @param string $error message + * @param boolean $string set to true if data must be string type + * @return boolean + */ + public function required($error, $string = TRUE) + { + if(empty($this->data[$this->field]) OR ($string AND is_array($this->data[$this->field]))) + { + $this->errors[$this->field] = $error; + } + + return $this; + } + + + /** + * Verify value is a string. + * + * @param mixed $data to validate + * @return boolean + */ + protected function string_rule($data) + { + return is_string($data); + } + + + /** + * Verify value is an array. + * + * @param mixed $data to validate + * @return boolean + */ + protected function array_rule($data) + { + return is_array($data); + } + + + /** + * Verify value is an integer + * + * @param string $data to validate + * @return boolean + */ + protected function integer_rule($data) + { + return is_int($data) OR ctype_digit($data); + } + + + /** + * Verifies the given date string is a valid date using the format provided. + * + * @param string $data to validate + * @param string $format of date string + * @return boolean + */ + protected function date_rule($data, $format = NULL) + { + if($format) + { + if($data = DateTime::createFromFormat($data, $format)) + { + return TRUE; + } + } + elseif($data = strtotime($data)) + { + return TRUE; + } + } + + + /** + * Condition must be true. + * + * @param mixed $data to validate + * @param boolean $condition to test + * @return boolean + */ + protected function true_rule($data, $condition) + { + return $condition; + } + + + /** + * Field must have a value matching one of the options + * + * @param mixed $data to validate + * @param array $array of posible values + * @return boolean + */ + protected function options_rule($data, $options) + { + return in_array($data, $options); + } + + + /** + * Validate that the given value is a valid IP4/6 address. + * + * @param mixed $data to validate + * @return boolean + */ + protected function ip_rule($data) + { + return (filter_var($data, FILTER_VALIDATE_IP) !== false); + } + + + /** + * Verify that the value of a field matches another one. + * + * @param mixed $data to validate + * @param string $field name of the other element + * @return boolean + */ + protected function matches_rule($data, $field) + { + if(isset($this->data[$field])) + { + return $data === $this->data[$field]; + } + } + + + /** + * Check to see if the email entered is valid. + * + * @param string $data to validate + * @return boolean + */ + protected function email_rule($data) + { + return preg_match('/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $data); + } + + + /** + * Must only contain word characters (A-Za-z0-9_). + * + * @param string $data to validate + * @return boolean + */ + protected function word_rule($data) + { + return preg_match("/\W/", $data); + } + + + /** + * Plain text that contains no HTML/XML "><" characters. + * + * @param string $data to validate + * @return boolean + */ + protected function plaintext_rule($data) + { + return (mb_strpos($data, '<') === FALSE AND mb_strpos($data, '>') === FALSE); + } + + + /** + * Minimum length of the string. + * + * @param string $data to validate + * @param int $length of the string + * @return boolean + */ + protected function min_rule($data, $length) + { + return mb_strlen($data) >= $length; + } + + + /** + * Maximum length of the string. + * + * @param string $data to validate + * @param int $length of the string + * @return boolean + */ + protected function max_rule($data, $length) + { + return mb_strlen($data) <= $length; + } + + + /** + * Exact length of the string. + * + * @param string $data to validate + * @param int $length of the string + * @return boolean + */ + protected function length_rule($data, $length) + { + return mb_strlen($data) === $length; + } + + + /** + * Tests a string for characters outside of the Base64 alphabet + * as defined by RFC 2045 http://www.faqs.org/rfcs/rfc2045 + * + * @param string $data to validate + * @return boolean + */ + protected function base64_rule($data) + { + return preg_match('/[^a-zA-Z0-9\/\+=]/', $data); + } + +} + +// END diff --git a/Class/Core/View.php b/Class/Core/View.php new file mode 100755 index 0000000..3849427 --- /dev/null +++ b/Class/Core/View.php @@ -0,0 +1,68 @@ +__view = $file; + } + + + /** + * Set an array of values + * + * @param array $array of values + */ + public function set($array) + { + foreach($array as $k => $v) + { + $this->$k = $v; + } + } + + + /** + * Return the view's HTML + * + * @return string + */ + public function __toString() + { + try { + ob_start(); + extract((array) $this); + require SP . "View/" . $this->__view . EXT; + return ob_get_clean(); + } + catch(\Exception $e) + { + Error::exception($e); + return ''; + } + } + +} + +// END diff --git a/Class/Core/XML.php b/Class/Core/XML.php new file mode 100755 index 0000000..89ef6dc --- /dev/null +++ b/Class/Core/XML.php @@ -0,0 +1,58 @@ +") + { + if(is_null($xml)) + { + $xml = simplexml_load_string("$doctype<$root/>"); + } + + foreach((array) $object as $k => $v) + { + if(is_int($k)) + { + $k = $unknown; + } + + if(is_scalar($v)) + { + $xml->addChild($k, h($v)); + } + else + { + $v = (array) $v; + $node = array_diff_key($v, array_keys(array_keys($v))) ? $xml->addChild($k) : $xml; + self::from($v, $k, $node); + } + } + + return $xml; + } + +} + +// END diff --git a/Class/Model/Permission.php b/Class/Model/Permission.php new file mode 100755 index 0000000..6838063 --- /dev/null +++ b/Class/Model/Permission.php @@ -0,0 +1,25 @@ + '\Model\Role', + 'resource' => '\Model\Resource', + ); + + /** + * Die scum! + */ + public function purge() + { + $i = static::$db->i; + + static::$db->query('TRUNCATE TABLE '. $i. static::$table. $i); + } + +} diff --git a/Class/Model/Resource.php b/Class/Model/Resource.php new file mode 100755 index 0000000..85d4dd2 --- /dev/null +++ b/Class/Model/Resource.php @@ -0,0 +1,22 @@ + 'desc'); + public static $cascade_delete = TRUE; + + public static $has = array( + 'permissions' => '\Model\Permission', + ); + + public static $has_many_through = array( + 'roles' => array( + 'resource_id' => '\Model\Permission', + 'role_id' => '\Model\Role' + ), + ); +} diff --git a/Class/Model/Role.php b/Class/Model/Role.php new file mode 100755 index 0000000..f396c32 --- /dev/null +++ b/Class/Model/Role.php @@ -0,0 +1,25 @@ + '\Model\Permission', + 'users' => '\Model\User', + ); + + public static $has_many_through = array( + 'resources' => array('\Model\Permission' => '\Model\Resource'), + ); + + public function has_permission($resource_id) + { + $i = static::$db->i; + + return (bool) static::$db->column('SELECT * FROM '. $i. 'permission'. $i. ' WHERE role_id = ? AND resource_id = ?', array($this->id, $resource_id)); + } +} diff --git a/Class/Model/User.php b/Class/Model/User.php new file mode 100755 index 0000000..3f5a412 --- /dev/null +++ b/Class/Model/User.php @@ -0,0 +1,14 @@ +quote("%$name%"); + return self::row(array("name LIKE $name")); + } +} diff --git a/Class/MyController.php b/Class/MyController.php new file mode 100755 index 0000000..2ab2e58 --- /dev/null +++ b/Class/MyController.php @@ -0,0 +1,80 @@ +$name); + + // Set default ORM database connection + if(empty(\Core\ORM::$db)) + { + \Core\ORM::$db = $db; + } + + return $db; + } + + + /** + * Show a 404 error page + */ + public function show_404() + { + headers_sent() OR header('HTTP/1.0 404 Page Not Found'); + $this->content = new \Core\View('404'); + } + + + /** + * Save user session and render the final layout template + */ + public function send() + { + \Core\Session::save(); + + headers_sent() OR header('Content-Type: text/html; charset=utf-8'); + + $layout = new \Core\View($this->template); + $layout->set((array) $this); + print $layout; + + $layout = NULL; + + if(config()->debug_mode) + { + print new \Core\View('System/Debug'); + } + } + +} + +// End diff --git a/Command/Backup.php b/Command/Backup.php new file mode 100755 index 0000000..ebf65d3 --- /dev/null +++ b/Command/Backup.php @@ -0,0 +1,25 @@ +connect(); + +// Set name of migration object +$migration = '\Core\Migration\\' . ($db->type == 'mysql' ? 'MySQL' : 'PGSQL'); + +// Create migration object +$migration = new $migration; + +// Set database connection +$migration->db = $db; + +// Set the database name +$migration->name = 'default'; + +// Load table configuration +$migration->tables = \Core\Config::load_all('Migration'); + +// Backup existing database table +$migration->backup_data(); diff --git a/Command/Create.php b/Command/Create.php new file mode 100755 index 0000000..db3512f --- /dev/null +++ b/Command/Create.php @@ -0,0 +1,25 @@ +connect(); + +// Set name of migration object +$migration = '\Core\Migration\\' . ($db->type == 'mysql' ? 'MySQL' : 'PGSQL'); + +// Create migration object +$migration = new $migration; + +// Set database connection +$migration->db = $db; + +// Set the database name +$migration->name = 'default'; + +// Load table configuration +$migration->tables = \Core\Config::load_all('Migration'); + +// Backup existing database table +$migration->create_schema(); diff --git a/Command/Restore.php b/Command/Restore.php new file mode 100755 index 0000000..12f0483 --- /dev/null +++ b/Command/Restore.php @@ -0,0 +1,25 @@ +connect(); + +// Set name of migration object +$migration = '\Core\Migration\\' . ($db->type == 'mysql' ? 'MySQL' : 'PGSQL'); + +// Create migration object +$migration = new $migration; + +// Set database connection +$migration->db = $db; + +// Set the database name +$migration->name = 'default'; + +// Load table configuration +$migration->tables = \Core\Config::load_all('Migration'); + +// Backup existing database table +$migration->restore_data(); diff --git a/Command/Run.php b/Command/Run.php new file mode 100755 index 0000000..5c04cb7 --- /dev/null +++ b/Command/Run.php @@ -0,0 +1,27 @@ +connect(); + +// Set name of migration object +$migration = '\Core\Migration\\' . ($db->type == 'mysql' ? 'MySQL' : 'PGSQL'); + +// Create migration object +$migration = new $migration; + +// Set database connection +$migration->db = $db; + +// Set the database name +$migration->name = 'default'; + +// Load table configuration +$migration->tables = \Core\Config::load_all('Migration'); + +// Backup existing database table +$migration->backup_data(); +$migration->create_schema(); +$migration->restore_data(); diff --git a/Common.php b/Common.php new file mode 100755 index 0000000..3c2ff88 --- /dev/null +++ b/Common.php @@ -0,0 +1,544 @@ +' . h($value === NULL ? 'NULL' : (is_scalar($value) ? $value : print_r($value, TRUE))) . "\n"; + } + return $string; +} + + +/** + * Safely fetch a $_POST value, defaulting to the value provided if the key is + * not found. + * + * @param string $key name + * @param mixed $default value if key is not found + * @param boolean $string TRUE to require string type + * @return mixed + */ +function post($key, $default = NULL, $string = FALSE) +{ + if(isset($_POST[$key])) + { + return $string ? str($_POST[$key], $default) : $_POST[$key]; + } + return $default; +} + + +/** + * Safely fetch a $_GET value, defaulting to the value provided if the key is + * not found. + * + * @param string $key name + * @param mixed $default value if key is not found + * @param boolean $string TRUE to require string type + * @return mixed + */ +function get($key, $default = NULL, $string = FALSE) +{ + if(isset($_GET[$key])) + { + return $string ? str($_GET[$key], $default) : $_GET[$key]; + } + return $default; +} + + +/** + * Safely fetch a $_SESSION value, defaulting to the value provided if the key is + * not found. + * + * @param string $k the post key + * @param mixed $d the default value if key is not found + * @return mixed + */ +function session($k, $d = NULL) +{ + return isset($_SESSION[$k]) ? $_SESSION[$k] : $d; +} + + +/** + * Create a random 32 character MD5 token + * + * @return string + */ +function token() +{ + return md5(str_shuffle(chr(mt_rand(32, 126)) . uniqid() . microtime(TRUE))); +} + + +/** + * Write to the application log file using error_log + * + * @param string $message to save + * @return bool + */ +function log_message($message) +{ + $path = SP . 'Storage/Log/' . date('Y-m-d') . '.log'; + + // Append date and IP to log message + return error_log(date('H:i:s ') . getenv('REMOTE_ADDR') . " $message\n", 3, $path); +} + + +/** + * Send a HTTP header redirect using "location" or "refresh". + * + * @param string $url the URL string + * @param int $c the HTTP status code + * @param string $method either location or redirect + */ +function redirect($url = NULL, $code = 302, $method = 'location') +{ + if(strpos($url, '://') === FALSE) + { + $url = site_url($url); + } + + //print dump($url); + + header($method == 'refresh' ? "Refresh:0;url = $url" : "Location: $url", TRUE, $code); +} + + +/* + * Return the full URL to a path on this site or another. + * + * @param string $uri may contain another sites TLD + * @return string + * +function site_url($uri = NULL) +{ + return (strpos($uri, '://') === FALSE ? \Core\URL::get() : '') . ltrim($uri, '/'); +} +*/ + +/** + * Return the full URL to a location on this site + * + * @param string $path to use or FALSE for current path + * @param array $params to append to URL + * @return string + */ +function site_url($path = NULL, array $params = NULL) +{ + // In PHP 5.4, http_build_query will support RFC 3986 + return DOMAIN . ($path ? '/'. trim($path, '/') : PATH) + . ($params ? '?'. str_replace('+', '%20', http_build_query($params, TRUE, '&')) : ''); +} + + +/** + * Return the current URL with path and query params + * + * @return string + * +function current_url() +{ + return DOMAIN . getenv('REQUEST_URI'); +} +*/ + +/** + * Convert a string from one encoding to another encoding + * and remove invalid bytes sequences. + * + * @param string $string to convert + * @param string $to encoding you want the string in + * @param string $from encoding that string is in + * @return string + */ +function encode($string, $to = 'UTF-8', $from = 'UTF-8') +{ + // ASCII is already valid UTF-8 + if($to == 'UTF-8' AND is_ascii($string)) + { + return $string; + } + + // Convert the string + return @iconv($from, $to . '//TRANSLIT//IGNORE', $string); +} + + +/** + * Tests whether a string contains only 7bit ASCII characters. + * + * @param string $string to check + * @return bool + */ +function is_ascii($string) +{ + return ! preg_match('/[^\x00-\x7F]/S', $string); +} + + +/** + * Encode a string so it is safe to pass through the URL + * + * @param string $string to encode + * @return string + */ +function base64_url_encode($string = NULL) +{ + return strtr(base64_encode($string), '+/=', '-_~'); +} + + +/** + * Decode a string passed through the URL + * + * @param string $string to decode + * @return string + */ +function base64_url_decode($string = NULL) +{ + return base64_decode(strtr($string, '-_~', '+/=')); +} + + +/** + * Convert special characters to HTML safe entities. + * + * @param string $string to encode + * @return string + */ +function h($string) +{ + return htmlspecialchars($string, ENT_QUOTES, 'utf-8'); +} + + +/** + * Filter a valid UTF-8 string so that it contains only words, numbers, + * dashes, underscores, periods, and spaces - all of which are safe + * characters to use in file names, URI, XML, JSON, and (X)HTML. + * + * @param string $string to clean + * @param bool $spaces TRUE to allow spaces + * @return string + */ +function sanitize($string, $spaces = TRUE) +{ + $search = array( + '/[^\w\-\. ]+/u', // Remove non safe characters + '/\s\s+/', // Remove extra whitespace + '/\.\.+/', '/--+/', '/__+/' // Remove duplicate symbols + ); + + $string = preg_replace($search, array(' ', ' ', '.', '-', '_'), $string); + + if( ! $spaces) + { + $string = preg_replace('/--+/', '-', str_replace(' ', '-', $string)); + } + + return trim($string, '-._ '); +} + + +/** + * Create a SEO friendly URL string from a valid UTF-8 string. + * + * @param string $string to filter + * @return string + */ +function sanitize_url($string) +{ + return urlencode(mb_strtolower(sanitize($string, FALSE))); +} + + +/** + * Filter a valid UTF-8 string to be file name safe. + * + * @param string $string to filter + * @return string + */ +function sanitize_filename($string) +{ + return sanitize($string, FALSE); +} + + +/** + * Return a SQLite/MySQL/PostgreSQL datetime string + * + * @param int $timestamp + */ +function sql_date($timestamp = NULL) +{ + return date('Y-m-d H:i:s', $timestamp ?: time()); +} + + +/** + * Make a request to the given URL using cURL. + * + * @param string $url to request + * @param array $options for cURL object + * @return object + */ +function curl_request($url, array $options = NULL) +{ + $ch = curl_init($url); + + $defaults = array( + CURLOPT_HEADER => 0, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_TIMEOUT => 5, + ); + + // Connection options override defaults if given + curl_setopt_array($ch, (array) $options + $defaults); + + // Create a response object + $object = new stdClass; + + // Get additional request info + $object->response = curl_exec($ch); + $object->error_code = curl_errno($ch); + $object->error = curl_error($ch); + $object->info = curl_getinfo($ch); + + curl_close($ch); + + return $object; +} + + +/** + * Create a RecursiveDirectoryIterator object + * + * @param string $dir the directory to load + * @param boolean $recursive to include subfolders + * @return object + */ +function directory($dir, $recursive = TRUE) +{ + $i = new \RecursiveDirectoryIterator($dir); + + if( ! $recursive) return $i; + + return new \RecursiveIteratorIterator($i, \RecursiveIteratorIterator::SELF_FIRST); +} + + +/** + * Make sure that a directory exists and is writable by the current PHP process. + * + * @param string $dir the directory to load + * @param string $chmod value as octal + * @return boolean + */ +function directory_is_writable($dir, $chmod = 0755) +{ + // If it doesn't exist, and can't be made + if(! is_dir($dir) AND ! mkdir($dir, $chmod, TRUE)) return FALSE; + + // If it isn't writable, and can't be made writable + if(! is_writable($dir) AND !chmod($dir, $chmod)) return FALSE; + + return TRUE; +} + + +/** + * Convert any given variable into a SimpleXML object + * + * @param mixed $object variable object to convert + * @param string $root root element name + * @param object $xml xml object + * @param string $unknown element name for numeric keys + * @param string $doctype XML doctype + */ +function to_xml($object, $root = 'data', $xml = NULL, $unknown = 'element', $doctype = "") +{ + if(is_null($xml)) + { + $xml = simplexml_load_string("$doctype<$root/>"); + } + + foreach((array) $object as $k => $v) + { + if(is_int($k)) + { + $k = $unknown; + } + + if(is_scalar($v)) + { + $xml->addChild($k, h($v)); + } + else + { + $v = (array) $v; + $node = array_diff_key($v, array_keys(array_keys($v))) ? $xml->addChild($k) : $xml; + self::from($v, $k, $node); + } + } + + return $xml; +} + + +/** + * Return an IntlDateFormatter object using the current system locale + * + * @param string $locale string + * @param integer $datetype IntlDateFormatter constant + * @param integer $timetype IntlDateFormatter constant + * @param string $timezone Time zone ID, default is system default + * @return IntlDateFormatter + */ +function __date($locale = NULL, $datetype = IntlDateFormatter::MEDIUM, $timetype = IntlDateFormatter::SHORT, $timezone = NULL) +{ + return new IntlDateFormatter($locale ?: setlocale(LC_ALL, 0), $datetype, $timetype, $timezone); +} + + +/** + * Format the given string using the current system locale + * Basically, it's sprintf on i18n steroids. + * + * @param string $string to parse + * @param array $params to insert + * @return string + */ +function __($string, array $params = NULL) +{ + return msgfmt_format_message(setlocale(LC_ALL, 0), $string, $params); +} + + +/** + * Color output text for the CLI + * + * @param string $text to color + * @param string $color of text + * @param string $background color + */ +function colorize($text, $color, $bold = FALSE) +{ + // Standard CLI colors + $colors = array_flip(array(30 => 'gray', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white', 'black')); + + // Escape string with color information + return"\033[" . ($bold ? '1' : '0') . ';' . $colors[$color] . "m$text\033[0m"; +} + +// End diff --git a/Config/Sample.Config.php b/Config/Sample.Config.php new file mode 100755 index 0000000..415db37 --- /dev/null +++ b/Config/Sample.Config.php @@ -0,0 +1,76 @@ + "mysql:host=127.0.0.1;port=3306;dbname=micromvc", + 'username' => 'root', + 'password' => '', + //'dns' => "pgsql:host=localhost;port=5432;dbname=micromvc", + //'username' => 'postgres', + //'password' => 'postgres', + 'params' => array() +); + + +/** + * System Events + */ +$config['events'] = array( + //'pre_controller' => 'Class::method', + //'post_controller' => 'Class::method', +); + +/** + * Cookie Handling + * + * To insure your cookies are secure, please choose a long, random key! + * @link http://php.net/setcookie + */ +$config['cookie'] = array( + 'key' => 'very-secret-key', + 'timeout' => time()+(60*60*4), // Ignore submitted cookies older than 4 hours + 'expires' => 0, // Expire on browser close + 'path' => '/', + 'domain' => '', + 'secure' => '', + 'httponly' => '', +); + + +/** + * API Keys and Secrets + * + * Insert you API keys and other secrets here. + * Use for Akismet, ReCaptcha, Facebook, and more! + */ + +//$config['XXX_api_key'] = '...'; + diff --git a/Config/Sample.Migration.php b/Config/Sample.Migration.php new file mode 100644 index 0000000..7922468 --- /dev/null +++ b/Config/Sample.Migration.php @@ -0,0 +1,29 @@ + 'primary|string|integer|boolean|decimal|datetime', + 'length' => NULL, + 'index' => FALSE, + 'null' => TRUE, + 'default' => NULL, + 'unique' => FALSE, + 'precision' => 0, // (optional, default 0) The precision for a decimal (exact numeric) column. (Applies only if a decimal column is used.) + 'scale' => 0, // (optional, default 0) The scale for a decimal (exact numeric) column. (Applies only if a decimal column is used.) +); +*/ + +$config = array( + + 'test_table' => array( + 'id' => array('type' => 'primary'), + 'title' => array('type' => 'string', 'length' => 100), + 'text' => array('type' => 'string'), + 'created' => array('type' => 'datetime'), + 'modified' => array('type' => 'datetime'), + ), + + + + +); diff --git a/Config/Sample.Route.php b/Config/Sample.Route.php new file mode 100644 index 0000000..2ab86c9 --- /dev/null +++ b/Config/Sample.Route.php @@ -0,0 +1,33 @@ + 'Forum\Controller\Forum\View' + * Result: Forum\Controller\Forum\View->action('45', 'Hello-World'); + * + ** Regex Example ** + * URL Path: /John_Doe4/recent/comments/3 + * Route: "/^(\w+)/recent/comments/' => 'Comments\Controller\Recent' + * Result: Comments\Controller\Recent->action($username = 'John_Doe4', $page = 3) + */ +$config = array(); + +$config['routes'] = array( + '' => '\Controller\Index', + '404' => '\Controller\Page404', + + // Example paths + //'example/path' => '\Controller\Example\Hander', + //'example/([^/]+)' => '\Controller\Example\Param', +); diff --git a/Locale/.gitignore b/Locale/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Public/index.php b/Public/index.php index 72c705c..c77170e 100755 --- a/Public/index.php +++ b/Public/index.php @@ -2,72 +2,37 @@ /** * Index * - * This file defines the MVC processing logic for the system + * This file defines the basic processing logic flow for the system * * @package MicroMVC * @author David Pennington - * @copyright (c) 2010 MicroMVC Framework + * @copyright (c) 2011 MicroMVC Framework * @license http://micromvc.com/license ********************************** 80 Columns ********************************* */ -// System Start Time -define('START_TIME', microtime(true)); - -// System Start Memory -define('START_MEMORY_USAGE', memory_get_usage()); - -// Extension of all PHP files -define('EXT', '.php'); - -// Absolute path to the system folder -define('SP', realpath(dirname(__DIR__)). DIRECTORY_SEPARATOR); - -// Are we using windows? -define('WINDOWS', strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); // Include bootstrap -require(SP . 'common' . EXT); - -//Is this an AJAX request? -define('AJAX_REQUEST', strtolower(getenv('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest'); - -// Custom init script? -if(config('init')) require(SP . 'init' . EXT); +require('../Bootstrap.php'); -// Register events -foreach(config('events') as $event => $class) +try { - event($event, '', $class); -} - -// Load App routes config file -$route = new \Core\Config('routes', 'App'); - -// Load router while removing route config (to free memory) -$route = new \Core\Route($route->array); + // Anything else before we start? + event('system.startup'); -// Parse the routes to find the correct controller while removing route object -list($params, $route, $controller) = $route->parse(\Core\URL::path()); + // Load controller dispatch passing URL routes + $dispatch = new \Core\Dispatch(config('Route')->routes); -// Any else before we start? -event('pre_controller', $controller); + // Run controller based on URL path and HTTP request method + $controller = $dispatch->controller(PATH, getenv('REQUEST_METHOD')); -// Load and run action -$controller = new $controller($route); + // Send the controller response + $controller->send(); -if($params) -{ - call_user_func_array(array($controller, 'action'), $params); + // One last chance to do something + event('system.shutdown', $controller); } -else +catch (Exception $e) { - $controller->action(); + \Core\Error::exception($e); } -// Render output -$controller->render(); - -// One last chance to do something -event('post_controller', $controller); - -// End diff --git a/README/install.txt b/README/install.txt index f39e99a..2e84558 100755 --- a/README/install.txt +++ b/README/install.txt @@ -1,15 +1,13 @@ --- Installing MicroMVC --- -Copy "sample.config.php" to "config.php". +Rename /Config/Sample.*.php files and edit with your configuration values. -To get the MicroMVC framework up and running you must setup the server vhost using Apache, Lighttpd, or Nginx. +The root web folder is /Public where all the Javascript, Images, and CSS should go. -If you are using the Apache web server you must rename "sample.htaccess" to ".htaccess" and edit the file to match your requirements. The most important thing to set is the "RewriteBase" base path on line 37. If micromvc is installed at http://micromvc.com then the base path can be left as-is ("RewriteBase /"). However, if you were installing in http://micromvc.com/framework/ then line 37 would read "RewriteBase /framework". +Sample server configurations are provided for Nginx and Apache2. -If you are using Nginx then you must already know what you're doing and you can use the sample.nginx.conf as a starting point for getting MicroMVC running. +A CLI console is provided for using the migrations -Sorry, but Lighttpd users will have to figure the rest out on their own until someone writes a sample vhost config. +$ php CLI create -After that, you must rename sample.config.php to config.php and edit it to suit your requirements. This is the core configuration file for the micromvc system and stored global info like your database connection data. - -Note: If you are using git to manage all files in your directory it is recommended that you keep the .htaccess and config.php files OUT of the repository since they will change for each new deploy environment (dev, staging, production, etc...). \ No newline at end of file +The code is very well commented, please read it. diff --git a/README/sample.htaccess b/README/sample.htaccess index c760c48..44022c3 100755 --- a/README/sample.htaccess +++ b/README/sample.htaccess @@ -5,20 +5,6 @@ # 1and1.com users might need to un-comment this line #AddType x-mapp-php5 .php -# Do not add an ending slash to directories (/) -#DirectorySlash Off - -# Hide the following files from the world - - Order Allow,Deny - Deny from all - - -# Allow the index.php file - - allow from All - - # Disable directory browsing Options All -Indexes @@ -26,7 +12,7 @@ Options All -Indexes #IndexIgnore * # Set the default file for indexes -DirectoryIndex index.php index.html +DirectoryIndex index.php index.html @@ -36,71 +22,6 @@ DirectoryIndex index.php index.html # The RewriteBase of the system (change if you are using this sytem in a sub-folder). RewriteBase / - # Hide svn and git folders/files - RedirectMatch 404 /\\.(svn|git) - - # Force error messages to load site pages? (optional) - #ErrorDocument 400 / - #ErrorDocument 401 / - #ErrorDocument 403 / - #ErrorDocument 404 / - #ErrorDocument 500 / - - ############################# - # Search engine optimization (SEO) - # - # Allowing access to pages (and sites) gives rise to multiple ways to - # reach a page - which is bad SEO! To prevent duplicate content issues - # the following rules attempt to correct this. May also require changes - # to the routes config in some cases. - # - # The following rules handle URI paths, slashes, and the WWW prefix. - # If you are hosting multiple sites then make sure you add them below. - # - - # Enforce www - # If you have subdomains, you can add them to the list using the "|" (OR) regex operator - #RewriteCond %{HTTP_HOST} !^(www|subdomain) [NC] - #RewriteRule ^(.*)$ http://www.micromvc.com/$1 [L,R=301] - - # Enforce NO www - #RewriteCond %{HTTP_HOST} ^www\.micromvc [NC] - #RewriteRule ^(.*)$ http://micromvc.com/$1 [L,R=301] - - ############################# - # Due to the routing system, the following URI all point to the index: - # / - # /welcome - # /welcome/ - # /welcome/index - # /welcome/index/ - # /index.php - - # This rule will make sure they redirect to "/". Change "welcome" if - # your default controller is named something else. - #RewriteRule ^(welcome(/index)?|index(\.php)?)/?$ / [L,R=301] - #RewriteRule ^(.*)/index/?$ $1 [L,R=301] - - ############################# - # Force URI to end with or without trailing slashes. Enable one method below only. - - # Method 1: Forces trailing slashes - #RewriteCond %{REQUEST_FILENAME} !-f - #RewriteCond %{REQUEST_URI} !/$ - #RewriteRule ^(.+)$ $1/ [L,R=301] - - # Method 2: Removes trailing slashes (recommended) - #RewriteCond %{REQUEST_FILENAME} !-d - #RewriteCond %{REQUEST_URI} /$ - #RewriteRule ^(.+)/$ $1 [L,R=301] - - ############################# - # Hide all PHP files - #RewriteCond %{REQUEST_FILENAME} !index.php - #RewriteCond %{REQUEST_FILENAME} !-d - #RewriteRule (.*)\.php$ - [L,F] - - ############################# # If the file/dir does not exist, route everything to index.php RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d @@ -110,7 +31,7 @@ DirectoryIndex index.php index.html ############################# # Prevent Image hotlinking (must be blank refer or this site) #RewriteCond %{HTTP_REFERER} !^$ - #RewriteCond %{HTTP_REFERER} !^http://(micromvc|othersite) [NC] + #RewriteCond %{HTTP_REFERER} !^http://(micromvc|othersite) [NC] #RewriteRule .*\.(gif|jpg|png)$ [NC,F] ############################# diff --git a/README/sample.nginx.conf b/README/sample.nginx.conf index c03a820..f00c4b2 100755 --- a/README/sample.nginx.conf +++ b/README/sample.nginx.conf @@ -1,28 +1,39 @@ -# MicroMVC Framework +# Basic server setup for domain "servername.tld" server { listen 80; - server_name micromvc.loc; + server_name servername.tld; + root /home/user/www/$host/Public; index index.html index.php; - # web root directory - root /var/www/micromvc/Public; + # Directives to send expires headers and turn off 404 error logging. + #location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { + # expires 24h; + # log_not_found off; + #} - try_files $uri @missing; - - location @missing { - rewrite ^ /index.php$request_uri last; + # Route all requests for non-existent files to index.php + location / { + try_files $uri $uri/ /index.php$is_args$args; } - # This will only run if the below location doesn't (so anything other than /index.php) - location ~ \.php { - rewrite ^ /index.php$request_uri last; - } + # Pass PHP scripts to php-fastcgi listening on port 9000 + location ~ \.php$ { + + # Zero-day exploit defense. + # http://forum.nginx.org/read.php?2,88845,page=3 + # Won't work properly (404 error) if the file is not stored on + # this server, which is entirely possible with php-fpm/php-fcgi. + # Comment the 'try_files' line out if you set up php-fpm/php-fcgi + # on another machine. And then cross your fingers that you won't get hacked. + try_files $uri =404; - # Route all index.php requests to the PHP processor - location ^~ /index.php { - include fastcgi.conf; # Notice that I changed the file + include fastcgi_params; fastcgi_pass 127.0.0.1:9000; } } -# Debian 5 users need to install the backports nginx package to use "try_files" +# PHP search for file Exploit: +# The PHP regex location block fires instead of the try_files block. Therefore we need +# to add "try_files $uri =404;" to make sure that "/uploads/virusimage.jpg/hello.php" +# never executes the hidden php code inside virusimage.jpg because it can't find hello.php! +# The exploit also can be stopped by adding "cgi.fix_pathinfo = 0" in your php.ini file. diff --git a/View/404.php b/View/404.php new file mode 100755 index 0000000..03f85a2 --- /dev/null +++ b/View/404.php @@ -0,0 +1,2 @@ +

Page Not Found

+

Sorry, we could not find the page you were looking for.

\ No newline at end of file diff --git a/View/Index/Index.php b/View/Index/Index.php new file mode 100755 index 0000000..f19bc56 --- /dev/null +++ b/View/Index/Index.php @@ -0,0 +1,2 @@ +

Welcome to MicroMVC

+

If you can see this then your install must be working! Try clicking on the links above to see some example uses.

\ No newline at end of file diff --git a/View/Layout.php b/View/Layout.php new file mode 100644 index 0000000..bccb5c7 --- /dev/null +++ b/View/Layout.php @@ -0,0 +1,51 @@ + + + + + MicroMVC + + + + + + '; + + //Print all JS files + if( ! empty($javascript)) foreach($javascript as $file) print ''; + + //Print any other header data + if( ! empty($head_data)) print $head_data; + ?> + + + + + + + +
+ +
+ + + + + +
+ +
+ + + + + +'. $debug. '';?> + + + diff --git a/View/Sidebar.php b/View/Sidebar.php new file mode 100755 index 0000000..d72107e --- /dev/null +++ b/View/Sidebar.php @@ -0,0 +1,6 @@ +

What is this?

+

Well, most people call it a "sidebar" and place information about +authors or sites here. Lately a lot of people have been using it for +twitter updates or flickr feeds.

+ +

Today is

diff --git a/View/System/Debug.php b/View/System/Debug.php new file mode 100755 index 0000000..fb8f974 --- /dev/null +++ b/View/System/Debug.php @@ -0,0 +1,71 @@ +
+ +Memory Usage +
+ bytes
+ bytes (process)
+ bytes (process peak)
+
+ +Execution Time +
 seconds
+ +URL Path + + +Locale + + +Timezone + + +', TRUE),36)); + }; + + foreach(\Core\Database::$queries as $type => $queries) + { + print ''.$type.' ('. count($queries). ' queries)'; + foreach($queries as $data) + { + print '
'. $highlight(wordwrap($data[1])."\n/* ".round(($data[0]*1000), 2).'ms */'). '
'; + } + } + + if(\Core\Error::$found) + { + print 'Last Query Run'; + print '
'. $highlight(\Core\DataBase::$last_query). '
'; + } +} +?> + + +$_POST Data + + + + +$_GET Data + + + + +Session Data + + + + + PHP Files Included: +
+
+
+ +Server Info + + +
diff --git a/View/System/Error.php b/View/System/Error.php new file mode 100755 index 0000000..c06d187 --- /dev/null +++ b/View/System/Error.php @@ -0,0 +1,69 @@ + + +
+ + Error +

+ + + + $line) + { + print '
'; + + //Skip the first element + if( $id !== 0 ) + { + // If this is a class include the class name + print 'Called by '. (isset($line['class']) ? $line['class']. $line['type'] : ''); + print $line['function']. '()'; + } + + // Print file, line, and source + print ' in '. $line['file']. ' ['. $line['line']. ']'; + print ''. $line['source']. ''; + + if(isset($line['args'])) + { + print 'Function Arguments'; + print dump($line['args']); + } + + print '
'; + } + + } + elseif(isset($file, $line)) + { + print '

'. $file. ' ('. $line. ')

'; + } + ?> + +
diff --git a/View/System/Exception.php b/View/System/Exception.php new file mode 100755 index 0000000..669ec88 --- /dev/null +++ b/View/System/Exception.php @@ -0,0 +1,73 @@ + + +
+ + +

getMessage(); ?>

+ + + getTrace()) + { + foreach($backtrace as $id => $line) + { + if(!isset($line['file'],$line['line']))continue; + + $x = TRUE; + + print '
'; + + //Skip the first element + if( $id !== 0 ) + { + // If this is a class include the class name + print 'Called by '. (isset($line['class']) ? $line['class']. $line['type'] : ''); + print $line['function']. '()'; + } + + // Print file, line, and source + print ' in '. $line['file']. ' ['. $line['line']. ']'; + print ''. \Core\Error::source($line['file'], $line['line']). ''; + + if(isset($line['args'])) + { + print 'Function Arguments'; + print dump($line['args']); + } + + print '
'; + } + + } + + if(!$x) + { + print '

'.$exception->getFile().' ('.$exception->getLine().')

'; + } + ?> + +