diff --git a/.ai/conventions.md b/.ai/conventions.md new file mode 100644 index 0000000..f797d60 --- /dev/null +++ b/.ai/conventions.md @@ -0,0 +1,32 @@ +# Neuron Framework - Coding Conventions + +## Code Style +- Tab indentation (not spaces) +- Opening braces on same line for class declarations, new line for methods/control structures +- Spaces before parentheses in function calls: `function_name ($arg)` +- Spaces around operators and after commas +- PHP closing tag `?>` used in source files +- PSR-4 autoloading under `Neuron\` namespace mapped to `src/Neuron/` + +## Input Validation Pattern +All user input goes through `Tools::checkInput()` for validation, then `Tools::getInput()` for retrieval: +1. `checkInput($value, $type)` returns bool - validates format +2. `getInput($data, $key, $type, $default)` returns validated+processed value or default + +Supported types: text, varchar, string, html, name, email, username, password, date, datetime, number, int, md5, base64, url, bool, raw + +## Database Queries +- Use `Query` class for parameterized queries +- Parameter types: `PARAM_STR`, `PARAM_NUMBER`, `PARAM_DATE`, `PARAM_POINT` +- Supports named parameters (`:name`) and positional (`?`) placeholders + +## Testing +- Tests extend `PHPUnit\Framework\TestCase` +- Test namespace: `Neuron\Tests` +- Database-dependent tests use `#[Group('database')]` attribute +- Run: `vendor/bin/phpunit --exclude-group=database` + +## Collections +- Base `Collection` class is observable (extends `Observable`) +- Triggers events: 'add', 'set', 'unset' +- Implements Iterator, ArrayAccess, Countable interfaces diff --git a/.ai/project-summary.md b/.ai/project-summary.md new file mode 100644 index 0000000..4f182bb --- /dev/null +++ b/.ai/project-summary.md @@ -0,0 +1,44 @@ +# Neuron Framework - Project Summary + +## Overview +Neuron is a lightweight PHP framework by CatLab Interactive. It provides core utilities for web applications including input validation, database query building, collections, encryption, URL building, filtering, and localization. + +## Architecture + +### Core Components +- **Application** (`src/Neuron/Application.php`) - Main application dispatcher, singleton pattern +- **Config** (`src/Neuron/Config.php`) - Configuration loader with dot-notation access and environment overrides +- **Router** (`src/Neuron/Router.php`) - URL routing +- **URLBuilder** (`src/Neuron/URLBuilder.php`) - Static URL construction utilities + +### Input Handling +- **Tools** (`src/Neuron/Core/Tools.php`) - Input validation (`checkInput`) and retrieval (`getInput`) for types: text, varchar, string, html, name, email, username, password, date, datetime, number, int, md5, base64, url, bool, raw + +### Database +- **Query** (`src/Neuron/DB/Query.php`) - Parameterized SQL query builder with INSERT, UPDATE, DELETE, SELECT support +- **Database** (`src/Neuron/DB/Database.php`) - Database interface +- **MySQL** (`src/Neuron/DB/MySQL.php`) - MySQL implementation + +### Collections +- **Collection** (`src/Neuron/Collections/Collection.php`) - Observable collection implementing Iterator, ArrayAccess, Countable +- **ModelCollection** (`src/Neuron/Collections/ModelCollection.php`) - Model-specific collection with ID indexing +- **ErrorCollection** (`src/Neuron/Collections/ErrorCollection.php`) - Error message collection + +### Security +- **SimpleCrypt** (`src/Neuron/Encryption/SimpleCrypt.php`) - AES-256-CBC encryption/decryption +- **TokenGenerator** (`src/Neuron/Tools/TokenGenerator.php`) - Random token generation + +### Filtering +- **Filter Parser/Scanner** (`src/Neuron/Filter/`) - Expression-based filtering with context support + +## Testing +- Tests are in `tests/` directory using PHPUnit 10/11 +- Bootstrap in `tests/bootstrap.php` +- Database tests are grouped with `#[Group('database')]` and excluded from CI +- Run tests: `vendor/bin/phpunit --exclude-group=database` + +## Build & Dependencies +- PHP >= 8.1 +- Composer for dependency management +- Key dependencies: `ext-gettext` +- Dev dependency: `phpunit/phpunit` diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ddb5ba2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,36 @@ +name: Tests + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + php-version: ['7.4', '8.0', '8.1', '8.2', '8.3'] + + name: PHP ${{ matrix.php-version }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: gettext, openssl, mbstring + coverage: xdebug + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run tests + run: vendor/bin/phpunit --exclude-group=database diff --git a/.gitignore b/.gitignore index 4c36e38..9df1b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ vendor/ +composer.lock +.phpunit.cache \ No newline at end of file diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..ea6dd77 --- /dev/null +++ b/claude.md @@ -0,0 +1,43 @@ +# Claude AI Assistant Guide for Neuron Framework + +This document provides context for AI assistants working with the Neuron framework codebase. + +## Project Documentation + +- [Project Summary](.ai/project-summary.md) - Architecture overview and component descriptions +- [Coding Conventions](.ai/conventions.md) - Code style, patterns, and testing practices + +## Quick Start + +### Install Dependencies +```bash +composer install +``` + +### Run Tests +```bash +# Run all tests (excluding database-dependent tests) +vendor/bin/phpunit --exclude-group=database + +# Run specific test file +vendor/bin/phpunit tests/ToolsTest.php + +# Run with coverage +vendor/bin/phpunit --exclude-group=database --coverage-text +``` + +## Key Areas + +### Input Validation (`src/Neuron/Core/Tools.php`) +The primary input validation layer. All user input should be validated through `Tools::checkInput()` before use. The `Tools::getInput()` method combines validation with data retrieval and type-specific processing. + +**Important**: Date validation uses `ctype_digit()` and `checkdate()` to ensure all date parts are valid integers representing a real date. The `getInput()` method casts date parts to `(int)` before passing to `mktime()` as a defence-in-depth measure. + +### Database Queries (`src/Neuron/DB/Query.php`) +Always use parameterized queries via the `Query` class to prevent SQL injection. Never concatenate user input directly into SQL strings. + +### Testing +Tests are located in `tests/` and use PHPUnit. Database-dependent tests are grouped with `#[Group('database')]` and require a MySQL connection. CI runs tests excluding this group. + +## CI/CD +GitHub Actions workflow runs tests on PHP 8.1, 8.2, and 8.3. See `.github/workflows/tests.yml`. diff --git a/composer.json b/composer.json index 739235b..b849047 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,25 @@ "role" : "Developer" } ], + + "require" : { + "php": ">=7.4", + "ext-gettext" : "*" + }, + + "require-dev": { + "phpunit/phpunit": "^9.5||^10.0||^11.0" + }, + "autoload": { - "psr-0" : { - "Neuron" : "src" + "psr-4" : { + "Neuron\\" : "src/Neuron/" + } + }, + + "autoload-dev": { + "psr-4": { + "Neuron\\Tests\\" : "tests/" } } -} \ No newline at end of file +} diff --git a/example/app/Example/Controllers/HomeController.php b/example/app/Example/Controllers/HomeController.php new file mode 100644 index 0000000..5254b3c --- /dev/null +++ b/example/app/Example/Controllers/HomeController.php @@ -0,0 +1,40 @@ +set ('title', Config::get ('app.name')); + + $template->set ('first', Config::get ('app.example.first')); + $template->set ('second', Config::get ('app.example.second')); + $template->set ('third', Config::get ('app.example.third')); + + $template->set ('counts', Config::get ('app.example')); + + return Response::template ($template); + } + + public function templates () + { + Template::addPath ('lowpriority', '', -5); + Template::addPath ('regularpriority', '', 0); + Template::addPath ('highpriority', '', 5); + + return Response::table (Template::getPaths()); + } +} \ No newline at end of file diff --git a/example/bootstrap/router.php b/example/bootstrap/router.php new file mode 100644 index 0000000..4b52b6e --- /dev/null +++ b/example/bootstrap/router.php @@ -0,0 +1,13 @@ +get ('/', '\Example\Controllers\HomeController@main'); +$router->get ('/templates', '\Example\Controllers\HomeController@templates'); + +$router->get ('/test/{something?}', function ($a) { + return \Neuron\Net\Response::json ($a); +}); + +return $router; \ No newline at end of file diff --git a/example/bootstrap/start.php b/example/bootstrap/start.php new file mode 100644 index 0000000..a542363 --- /dev/null +++ b/example/bootstrap/start.php @@ -0,0 +1,40 @@ +add ('Example\\', __DIR__ . '/../app/'); + +// Start the app +$app = \Neuron\Application::getInstance (); + +// Load the router +$app->setRouter (include ('router.php')); + +// Set config folder +\Neuron\Config::folder (__DIR__ . '/../config/'); + +// Optionally, set an environment +$hostname = trim (file_get_contents ('/etc/hostname')); + +switch ($hostname) +{ + case 'my-computer': + case 'thijs-home-i7': + \Neuron\Config::environment ('development'); + break; +} + +// Set the template folder +\Neuron\Core\Template::addPath (__DIR__ . '/../templates/'); + +// Set the locale +$app->setLocale ('nl_BE.utf8'); + +// Set our own domain +\Neuron\Tools\Text::getInstance ()->addPath ('example', __DIR__ . '/../locales/'); + +// Return app +return $app; \ No newline at end of file diff --git a/example/config/app.php b/example/config/app.php new file mode 100644 index 0000000..eda23f4 --- /dev/null +++ b/example/config/app.php @@ -0,0 +1,15 @@ + '/', + + 'name' => 'Neuron example', + + 'example' => array ( + + 'first' => 'First value', + 'second' => 'Second value' + + ) + +); \ No newline at end of file diff --git a/example/config/database.php b/example/config/database.php new file mode 100644 index 0000000..1e4195b --- /dev/null +++ b/example/config/database.php @@ -0,0 +1,14 @@ + array ( + 'host' => 'localhost', + 'username' => 'myuser', + 'password' => 'myuser', + 'database' => 'catlab_accounts', + 'charset' => 'utf8', + 'prefix' => 'neuron_' + ) + +); \ No newline at end of file diff --git a/example/config/development/app.php b/example/config/development/app.php new file mode 100644 index 0000000..989bfe6 --- /dev/null +++ b/example/config/development/app.php @@ -0,0 +1,12 @@ + 'Neuron example development environment', + + 'example' => array ( + + 'third' => 'Third value' + + ) + +); \ No newline at end of file diff --git a/example/index.php b/example/index.php deleted file mode 100644 index 6f0a395..0000000 --- a/example/index.php +++ /dev/null @@ -1,27 +0,0 @@ -get ('/response', function () { - - return \Neuron\Net\Response::json (array ('test')); - -}); - -$router->get ('/string', function () { - - return 'String'; - -}); - -$router->get ('/void', function () { - - echo 'void'; - -}); - -$router->run (); \ No newline at end of file diff --git a/example/locales/nl_BE/LC_MESSAGES/example.mo b/example/locales/nl_BE/LC_MESSAGES/example.mo new file mode 100644 index 0000000..cf1951b Binary files /dev/null and b/example/locales/nl_BE/LC_MESSAGES/example.mo differ diff --git a/example/locales/nl_BE/LC_MESSAGES/example.po b/example/locales/nl_BE/LC_MESSAGES/example.po new file mode 100644 index 0000000..f12a1b8 --- /dev/null +++ b/example/locales/nl_BE/LC_MESSAGES/example.po @@ -0,0 +1,55 @@ +msgid "" +msgstr "" +"Project-Id-Version: Neuron Example\n" +"POT-Creation-Date: 2014-11-29 23:17+0100\n" +"PO-Revision-Date: 2014-11-29 23:18+0100\n" +"Last-Translator: Thijs Van der Schaeghe \n" +"Language-Team: CatLab Interactive \n" +"Language: nl_BE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.4\n" +"X-Poedit-KeywordsList: _;gettext;gettext_noop\n" +"X-Poedit-Basepath: ../../../\n" +"X-Poedit-SourceCharset: UTF-8\n" +"Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;\n" +"X-Poedit-SearchPath-0: templates\n" + +#: templates/example.phpt:6 +msgid "Example structure" +msgstr "Voorbeeldstructuur" + +#: templates/example.phpt:8 +msgid "" +"This is just an example structure. We want to give you all the freedom you " +"need. No rigid structure. Just a bunch of classes that you can instanciate " +"in any way you like." +msgstr "" +"Dit is slechts een voorbeeldstructuur. We willen je alle vrijheid geven die " +"je nodig hebt. Geen lompe, verplichte structuren. Gewoon een hoop klasses " +"die je naar goeddunken kan gebruiken." + +#: templates/example.phpt:11 +msgid "Configuration" +msgstr "Configuratie" + +#: templates/example.phpt:12 +#, php-format +msgid "Config example: %s" +msgstr "Configuratie voorbeeld: %s" + +#: templates/example.phpt:19 +msgid "Text" +msgstr "Tekst" + +#: templates/example.phpt:20 +msgid "Let's give this gettext a try..." +msgstr "Laten we dit nog eens proberen..." + +#: templates/example.phpt:25 +#, php-format +msgid "There is %s beer" +msgid_plural "There are %s beers." +msgstr[0] "Er is %s bier." +msgstr[1] "Er zijn %s bieren." diff --git a/example/.htaccess b/example/public/.htaccess similarity index 100% rename from example/.htaccess rename to example/public/.htaccess diff --git a/example/public/assets/css/style.css b/example/public/assets/css/style.css new file mode 100644 index 0000000..bb9bcc6 --- /dev/null +++ b/example/public/assets/css/style.css @@ -0,0 +1,9 @@ +h1 +{ + color: red; +} + +h2 +{ + color: blue; +} \ No newline at end of file diff --git a/example/public/assets/js/main.js b/example/public/assets/js/main.js new file mode 100644 index 0000000..de04db9 --- /dev/null +++ b/example/public/assets/js/main.js @@ -0,0 +1 @@ +console.log ('javascript ok'); \ No newline at end of file diff --git a/example/public/datatype.php b/example/public/datatype.php new file mode 100644 index 0000000..1cef5f3 --- /dev/null +++ b/example/public/datatype.php @@ -0,0 +1,24 @@ +doSomething (); +} + +$mi = new Bar (); + diff --git a/example/public/index.php b/example/public/index.php new file mode 100644 index 0000000..a46b437 --- /dev/null +++ b/example/public/index.php @@ -0,0 +1,4 @@ +dispatch (); \ No newline at end of file diff --git a/example/public/urlbuilder.php b/example/public/urlbuilder.php new file mode 100644 index 0000000..48b2a03 --- /dev/null +++ b/example/public/urlbuilder.php @@ -0,0 +1,5 @@ + 'bar')); \ No newline at end of file diff --git a/example/templates/example.phpt b/example/templates/example.phpt new file mode 100644 index 0000000..ba25ce1 --- /dev/null +++ b/example/templates/example.phpt @@ -0,0 +1,25 @@ +layout ('index.phpt'); + $this->textdomain ('example'); +?> + +

gettext ('Example structure'); ?>

+

+ gettext ('This is just an example structure. We want to give you all the freedom you need. No rigid structure. Just a bunch of classes that you can instanciate in any way you like.'); ?> +

+ +

gettext ('Configuration'); ?>

+

gettext ('Config example: %s'), $title); ?>.

+ + +

gettext ('Text'); ?>

+

gettext ('Let\'s give this gettext a try...'); ?>

+ +

Multiple text

+ + +

ngettext ('There is %s beer', 'There are %s beers.', $beers), $beers); ?>

\ No newline at end of file diff --git a/example/templates/index.phpt b/example/templates/index.phpt new file mode 100644 index 0000000..3215911 --- /dev/null +++ b/example/templates/index.phpt @@ -0,0 +1,15 @@ + + + + + combine ('sections/head.phpt'); ?> + + + + + +

Neuron framework

+ + + + \ No newline at end of file diff --git a/example/templates/sections/head.phpt b/example/templates/sections/head.phpt new file mode 100644 index 0000000..e9a2461 --- /dev/null +++ b/example/templates/sections/head.phpt @@ -0,0 +1,9 @@ + + + css ('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css'); ?> + + css ('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap-theme.min.css'); ?> + + + + js ('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js'); ?> \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..45a41da --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,10 @@ + + + + + + tests + + + diff --git a/src/Neuron/Application.php b/src/Neuron/Application.php new file mode 100644 index 0000000..569d2a9 --- /dev/null +++ b/src/Neuron/Application.php @@ -0,0 +1,218 @@ +router = $router; + $router->setApplication($this); + + // Here we will make a special filter called 'session' that initiates the session handler if it is requested + // by the router. We will also make sure that the session filter is the first one to be loaded. + // Yes, this isn't perfect design, but this framework is 10-ish years old and only used for a few self made projects. + $this->router->addFilter('session', function(Filter $filter) { + + // Set session from the session handler + $this->session = new Session ($this->getSessionHandler()); + $this->session->connect(); + + // Set session in request + $filter->getRequest()->setSession($this->session); + + $this->trigger('dispatch:session', $filter->getRequest()); + return true; + + }, 100); + + $this->trigger ('router:set'); + + } + + /** + * @return Router + */ + public function getRouter () + { + return $this->router; + } + + /** + * @param string $locale + * @throws DataNotSet + */ + public function setLocale ($locale) + { + $this->locale = $locale; + + // Also let php know + putenv ("LANG=" . $this->locale); + putenv ("LANGUAGE=" . $this->locale); + + $result = setlocale (LC_ALL, $this->locale); + if (!$result) + { + throw new DataNotSet ("Locale " . $locale . " is not available on this platform."); + } + $this->trigger ('locale:set'); + } + + /** + * @return string + */ + public function getLocale () + { + return $this->locale; + } + + /** + * @return string + */ + public function getLanguage() + { + return substr($this->locale, 0, 2); + } + + /** + * Check if locale is set, and if not, set it to english. + */ + private function checkLocale () + { + if (!isset ($this->locale)) { + $this->setLocale ('en_GB.utf8'); + } + } + + /** + * @param SessionHandler $handler + */ + public function setSessionHandler (SessionHandler $handler) + { + $this->sessionHandler = $handler; + } + + /** + * @return SessionHandler + */ + private function getSessionHandler () + { + if (!isset ($this->sessionHandler)) + { + $this->sessionHandler = new SessionHandler (); + } + + return $this->sessionHandler; + } + + /** + * @param Request $request + * @throws DataNotSet + */ + public function dispatch (Request $request = null) + { + if ($this->isFirstDispatch) { + $this->isFirstDispatch = false; + $this->trigger ('dispatch:first'); + } + + // Trigger initialize + $this->trigger ('dispatch:initialize'); + + // Check locales + $this->checkLocale (); + + if (!isset ($this->router)) + { + throw new DataNotSet ("Application needs a router."); + } + + if (!isset ($request)) + { + $request = Request::fromInput (); + } + + // Trigger before + $this->trigger ('dispatch:before', $request); + + // Run router + $this->router->run ($request); + + // Trigger dispatch + $this->trigger ('dispatch:after', $request); + + // Disconnect the session + if ($this->session) { + $this->session->disconnect(); + } + + // End + $this->trigger ('dispatch:terminate'); + } + + /** + * @param Request $request + */ + public function postFilters(Request $request) + { + $this->trigger ('dispatch:post-filters', $request); + } +} diff --git a/src/Neuron/Auth/HttpAuth.php b/src/Neuron/Auth/HttpAuth.php new file mode 100644 index 0000000..a1a0def --- /dev/null +++ b/src/Neuron/Auth/HttpAuth.php @@ -0,0 +1,80 @@ +addFilter ('basicauth', array ($checker, 'check')); + } + + /** + * @param array $users + */ + public function __construct (array $users) + { + $this->setUsers ($users); + } + + /** + * @return array[] + */ + public function getUsers () + { + return $this->users; + } + + /** + * @param array[] $users + * @return HttpAuth + */ + public function setUsers ($users) + { + $this->users = $users; + return $this; + } + + private function getUser ($username) + { + foreach ($this->users as $user) + { + if ($user['username'] === $username) { + return $user; + } + } + return null; + } + + public function check (\Neuron\Models\Router\Filter $filter) + { + $error = new \Neuron\Net\Response (); + $error->setBody ('Authorization not accepted'); + $error->setHeader ('WWW-Authenticate', 'Basic realm="Secured zone"'); + $error->setHeader ('HTTP/1.0 401 Unauthorized'); + + if (!isset($_SERVER['PHP_AUTH_USER'])) { + return $error; + } + + else { + + $user = $this->getUser ($_SERVER['PHP_AUTH_USER']); + if (!$user) { + return $error; + } + + else { + if ($_SERVER['PHP_AUTH_PW'] !== $user['password']) { + return $error; + } + return true; + } + } + } + +} \ No newline at end of file diff --git a/src/Neuron/Collections/Collection.php b/src/Neuron/Collections/Collection.php index 7796ea3..75734f2 100644 --- a/src/Neuron/Collections/Collection.php +++ b/src/Neuron/Collections/Collection.php @@ -11,18 +11,24 @@ use ArrayAccess; use Countable; use Iterator; +use Neuron\Models\Observable; -abstract class Collection +class Collection + extends Observable implements Iterator, ArrayAccess, Countable { private $position = 0; - private $data; + private $data = array (); protected function setCollectionValues (array $data) { $this->data = $data; } + public function add ($value) { + $this[] = $value; + } + /** * (PHP 5 >= 5.0.0)
* Whether a offset exists @@ -70,11 +76,20 @@ public function offsetSet($offset, $value) { if (is_null ($offset)) { - $this->data[] = $value; + $index = array_push ($this->data, $value) - 1; + $this->trigger ('add', $value, $index); } else { - $this->data[$offset] = $value; + $this->data[$offset] = $value; + + if (isset ($this->data[$offset])) + { + $this->trigger ('add', $value, $offset); + } + else { + $this->trigger ('set', $value, $offset); + } } } @@ -89,7 +104,9 @@ public function offsetSet($offset, $value) */ public function offsetUnset($offset) { + $value = isset ($this->data[$offset]) ? $this->data[$offset] : null; unset ($this->data[$offset]); + $this->trigger ('unset', $value, $offset); } /** @@ -179,4 +196,66 @@ public function peek() } return null; } -} \ No newline at end of file + + public function reverse () + { + $this->data = array_reverse ($this->data); + } + + private function isAssociative () { + return array_values ($this->data) === $this->data; + } + + /** + * Remove an element from the collection. + * @param $entry + * @return bool + */ + public function remove ($entry) { + foreach ($this->data as $k => $v) { + if ($v === $entry) { + $associative = $this->isAssociative (); + + unset ($this->data[$k]); + if ($associative) { + $this->data = array_values ($this->data); + } + + return true; + + } + } + return false; + } + + /** + * Return the very first element. + */ + public function first () + { + if (!is_array($this->data)) return $this->data; + if (!count($this->data)) return null; + reset($this->data); + return $this->data[key($this->data)]; + } + + /** + * Return the very last element. + */ + public function last () + { + if (!is_array($this->data)) return $this->data; + if (!count($this->data)) return null; + end($this->data); + return $this->data[key($this->data)]; + } + + /** + * Clear array + */ + public function clear () + { + $this->data = array (); + $this->rewind (); + } +} diff --git a/src/Neuron/Collections/ErrorCollection.php b/src/Neuron/Collections/ErrorCollection.php new file mode 100644 index 0000000..721fe0e --- /dev/null +++ b/src/Neuron/Collections/ErrorCollection.php @@ -0,0 +1,72 @@ +add($error); + + return $error; + } + + /** + * @return array + */ + public function getDetailedData() + { + $out = array (); + foreach ($this as $v) { + if ($v instanceof Error) { + $out[] = [ + 'message' => $v->getMessage(), + 'template' => $v->getTemplate(), + 'arguments' => $v->getArguments(), + 'subject' => $v->getSubject(), + 'code' => $v->getCode() + ]; + } else { + $out[] = [ + 'message' => $v->getMessage(), + 'template' => $v->getMessage(), + 'arguments' => [], + 'subject' => null, + 'code' => null + ]; + } + } + return $out; + } + + /** + * @return string[] + */ + public function getData () + { + $out = array (); + foreach ($this as $v) + $out[] = (string) $v; + + return $out; + } +} diff --git a/src/Neuron/Collections/ModelCollection.php b/src/Neuron/Collections/ModelCollection.php new file mode 100644 index 0000000..40b91f4 --- /dev/null +++ b/src/Neuron/Collections/ModelCollection.php @@ -0,0 +1,71 @@ +on ('add', array ($this, 'onAdd')); + $this->on ('set', array ($this, 'onAdd')); + $this->on ('unset', array ($this, 'onUnset')); + } + + /** + * @param Model|null $model + * @param null $offset + */ + protected function onAdd (Model $model = null, $offset = null) + { + if ($model) { + $this->map[$model->getId ()] = $model; + } + } + + /** + * @param Model|null $model + * @param null $offset + */ + protected function onUnset (Model $model = null, $offset = null) + { + if ($model) { + unset ($this->map[$model->getId()]); + } + } + + /** + * Return all ids. + * @return array + */ + public function getIds () + { + return array_keys ($this->map); + } + + /** + * @param $id + * @return mixed|null + */ + public function getFromId ($id) + { + if (isset ($this->map[$id])) { + return $this->map[$id]; + } + return null; + } +} \ No newline at end of file diff --git a/src/Neuron/Collections/TokenizedCollection.php b/src/Neuron/Collections/TokenizedCollection.php new file mode 100644 index 0000000..4bac2cf --- /dev/null +++ b/src/Neuron/Collections/TokenizedCollection.php @@ -0,0 +1,51 @@ +tokens[$token])) + { + $i = 1; + $tmp = $token . $i; + while (isset ($this->tokens[$tmp])) + { + $i ++; + } + $token = $tmp; + } + + $this->tokens[$token] = true; + return $token; + } + +} \ No newline at end of file diff --git a/src/Neuron/Config.php b/src/Neuron/Config.php new file mode 100644 index 0000000..d709992 --- /dev/null +++ b/src/Neuron/Config.php @@ -0,0 +1,153 @@ +getValue ($name, $default); + } + + /** + * @param string $folder + */ + public static function folder ($folder) + { + self::getInstance ()->setFolder ($folder); + } + + /** + * Set the environment (sub folder) + * @param $environment + */ + public static function environment ($environment) + { + self::getInstance ()->setEnvironment ($environment); + } + + /** + * Get an instance. + * @return Config + */ + private static function getInstance () + { + if (!isset (self::$in)) + { + self::$in = new self (); + } + return self::$in; + } + + /** + * @param string $folder + */ + private function setFolder ($folder) + { + $this->folder = $folder; + } + + /** + * @param string $environment + */ + private function setEnvironment ($environment) + { + $this->environment = $environment; + } + + /** + * Load a file in case it's not loaded yet. + * @param $file + */ + private function loadFile ($file) + { + if (!isset ($this->files[$file])) + { + $filename = $this->folder . $file . '.php'; + + // First load these + if (file_exists ($filename)) + { + $this->files[$file] = include ($filename); + } + + // Now overload with environment values + if (isset ($this->environment)) + { + $filename = $this->folder . $this->environment . '/' . $file . '.php'; + if (file_exists ($filename)) + { + $this->merge ($file, include ($filename)); + } + } + + } + } + + /** + * @param string $file + * @param mixed[] $newData + */ + private function merge ($file, $newData) + { + $this->files[$file] = array_replace_recursive ($this->files[$file], $newData); + } + + /** + * Find a config variable and return it. + * @param string $name + * @param string $default + * @return mixed + */ + private function getValue ($name, $default) + { + $parts = explode ('.', $name); + $file = array_shift ($parts); + + $this->loadFile ($file); + + if (! isset ($this->files[$file])) { + return $default; + } + else { + $out = $this->files[$file]; + foreach ($parts as $part) + { + if (!isset ($out[$part])) + { + return $default; + } + else { + $out = $out[$part]; + } + } + } + + return $out; + } +} \ No newline at end of file diff --git a/src/Neuron/Core/DataUri.php b/src/Neuron/Core/DataUri.php deleted file mode 100644 index b115a32..0000000 --- a/src/Neuron/Core/DataUri.php +++ /dev/null @@ -1,264 +0,0 @@ -Lucas - */ -class DataUri { - - /** @var Regular expression used for decomposition of data URI scheme */ - private static $REGEX_URI = '/^data:(.+?){0,1}(?:(?:;(base64)\,){1}|\,)(.+){0,1}$/'; - - const DEFAULT_TYPE = 'text/plain;charset=US-ASCII'; - - const ENCODING_URL_ENCODED_OCTETS = 0; - const ENCODING_BASE64 = 1; - - /** @var Keyword used in the data URI to signify base64 encoding */ - const BASE64_KEYWORD = 'base64'; - - private $mediaType; - private $encoding; - private $encodedData; - - /** - * Instantiates an instance of the DataURI class, initialised with the - * default values defined in RFC 2397. That is the media-type of - * text/plain;charset=US-ASCII and encoding type of URL encoded octets. - * - * @param string $mediaType - * @param string $data Unencoded data - * @param integer $encoding Class constant of either - * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or - * {@link DataUri::ENCODING_BASE64} - * - * @throws InvalidArgumentException - */ - public function __construct($mediaType = DataUri::DEFAULT_TYPE, - $data = '', - $encoding = DataUri::ENCODING_URL_ENCODED_OCTETS - ) { - try { - $this->setMediaType($mediaType); - $this->setData($data, $encoding); - } catch (InvalidArgumentException $e) { - throw $e; - } - } - - /** - * Returns the data URI's media-type. If none was provided then in - * accordance to RFC 2397 it will default to text/plain;charset=US-ASCII - * - * @return string Media-type - */ - public function getMediaType() { - return empty($this->mediaType) === false - ? $this->mediaType - : DataUri::DEFAULT_TYPE; - } - - /** - * Sets the media-type. - * - * @param string $mediaType Media-type - */ - public function setMediaType($mediaType) { - $this->mediaType = $mediaType; - } - - /** - * Returns the method of encoding used for the data. - * - * @return int Class constant of either - * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or - * {@link DataUri::ENCODING_BASE64} - */ - public function getEncoding() { - return $this->encoding; - } - - /** - * Returns the data in its encoded form. - * - * @return string Encoded data - */ - public function getEncodedData() { - return $this->encodedData; - } - - /** - * Sets the encoded data and the encoding scheme used to encode/decode it. - * Be aware that the data is not validated, so ensure that the correct - * encoding scheme is provided otherwise the method - * {@link DataUri::tryDecodeData($decodedData)} will fail. - - * @param int $encoding Class constant of either - * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or - * {@link DataUri::ENCODING_BASE64} - * @param string $data Data encoded with the encoding scheme provided - * @throws InvalidArgumentException - */ - public function setEncodedData($encoding, $data) { - if(($encoding !== DataUri::ENCODING_URL_ENCODED_OCTETS) && - ($encoding !== DataUri::ENCODING_BASE64)) { - throw new InvalidArgumentException('Unsupported encoding scheme'); - } - - $this->encoding = $encoding; - $this->encodedData = $data; - } - - - /** - * Sets the data for the data URI, which it stores in encoded form using - * the encoding scheme provided. - * - * @param string $data Data to encode then store - * @param int $encoding Class constant of either - * {@link DataUri::ENCODING_URL_ENCODED_OCTETS} or - * {@link DataUri::ENCODING_BASE64} - * @throws InvalidArgumentException - */ - public function setData($data, $encoding = DataUri::ENCODING_URL_ENCODED_OCTETS) { - switch($encoding) { - case DataUri::ENCODING_URL_ENCODED_OCTETS: - $this->encoding = DataUri::ENCODING_URL_ENCODED_OCTETS; - $this->encodedData = rawurlencode($data); - break; - case DataUri::ENCODING_BASE64: - $this->encoding = DataUri::ENCODING_BASE64; - $this->encodedData = base64_encode($data); - break; - default: - throw new InvalidArgumentException('Unsupported encoding scheme'); - break; - } - } - - /** - * Tries to decode the URI's data using the encoding scheme set. - * - * @param null $decodedData Stores the decoded data - * @return boolean true if data was output, - * else false - */ - public function tryDecodeData(&$decodedData) { - $hasOutput = false; - - switch($this->getEncoding()) { - case DataUri::ENCODING_URL_ENCODED_OCTETS: - $decodedData = rawurldecode($this->getEncodedData()); - $hasOutput = true; - break; - case DataUri::ENCODING_BASE64: - $b64Decoded = base64_decode($this->getEncodedData(), true); - - if($b64Decoded !== false) { - $decodedData = $b64Decoded; - $hasOutput = true; - } - break; - default: - // NOP - break; - } - - return $hasOutput; - } - - /** - * Generates a data URI string representation of the object. - * - * @return string - */ - public function toString() { - $output = 'data:'; - - if(($this->getMediaType() !== DataUri::DEFAULT_TYPE) || - ($this->getEncoding() !== DataUri::ENCODING_URL_ENCODED_OCTETS)) { - $output .= $this->getMediaType(); - - if($this->getEncoding() === DataUri::ENCODING_BASE64) { - $output .= ';'.DataUri::BASE64_KEYWORD; - } - } - - $output .= ','.$this->getEncodedData(); - return $output; - } - - public function __toString() - { - return $this->toString(); - } - - /** - * Determines whether a string is data URI with the components necessary for - * it to be parsed by the {@link DataUri::tryParse($s, &$out)} method. - * - * @param string $string Data URI - * @return boolean true if possible to parse, - * else false - */ - public static function isParsable ($dataUriString) { - return (preg_match(DataUri::$REGEX_URI, $dataUriString) === 1); - } - - /** - * Parses a string data URI into an instance of a DataUri object. - * - * @param string $dataUriString Data URI to be parsed - * @param DataUri $out Output DataUri of the method - * @return boolean true if successful, else false - */ - public static function tryParse($dataUriString, &$out) { - $hasOutput = false; - - if(DataUri::isParsable($dataUriString)) { - $matches = null; - if(preg_match_all(DataUri::$REGEX_URI, - $dataUriString, - $matches, - PREG_SET_ORDER) !== false) { - - $mediatype = isset($matches[0][1]) - ? $matches[0][1] - : DataUri::DEFAULT_TYPE; - - $matchedEncoding = isset($matches[0][2]) ? $matches[0][2] : ''; - $encoding = (strtolower($matchedEncoding) === DataUri::BASE64_KEYWORD) - ? DataUri::ENCODING_BASE64 - : DataUri::ENCODING_URL_ENCODED_OCTETS; - - $data = isset($matches[0][3]) - ? $matches[0][3] - : ''; - - $dataUri = new DataUri(); - $dataUri->setMediaType($mediatype); - $dataUri->setEncodedData($encoding, $data); - - $out = $dataUri; - $hasOutput = true; - } - } - - return $hasOutput; - } -} \ No newline at end of file diff --git a/src/Neuron/Core/Error.php b/src/Neuron/Core/Error.php deleted file mode 100644 index 7c0c0f8..0000000 --- a/src/Neuron/Core/Error.php +++ /dev/null @@ -1,13 +0,0 @@ -template = $template; + + foreach ($values as $name => $value) { + $this->set ($name, $value); + } } - else + + /** + * Clear all shared variables. + */ + public static function clearShares () { - set_template_path (get_template_path () . PATH_SEPARATOR . $path); + self::$shares = array (); } -} - -// Backwards compatability stuff -if (defined ('DEFAULT_TEMPLATE_DIR')) -{ - add_to_template_path (DEFAULT_TEMPLATE_DIR, false); -} - -if (defined ('TEMPLATE_DIR')) -{ - add_to_template_path (TEMPLATE_DIR, true); -} -class Template -{ - - private $values = array (); - private $lists = array (); - - private $sTextFile = null; - private $sTextSection = null; - - private $objText = null; - - public static function load () + /** + * Set a variable that will be shared across all templates. + * @param $name + * @param $value + */ + public static function share ($name, $value) { - + self::$shares[$name] = $value; } /** - * I believe this is a nicer way to do the directory setting. - */ - public static function setTemplatePath ($path) + * Add a helper that is available inside the templates. + * @param $name + * @param $helper + */ + public static function addHelper ($name, $helper) { - set_template_path ($path); + self::$helpers[$name] = $helper; } /** - * Add a folder to the template path. - * @param $path: path to add - * @param $prefix: only templates starting with given prefix will be loaded from this path. - */ - public static function addTemplatePath ($path, $prefix, $priorize = false) + * I believe this is a nicer way to do the directory setting. + */ + public static function setPath ($path) { - add_to_template_path ($path, $priorize, $prefix); + self::$paths = array (); + self::$pathpriorities = array (); + + self::addPath ($path, '', 0); } - - public static function getUniqueId () + + /** + * Add a folder to the template path. + * @param $path: path to add + * @param $prefix: only templates starting with given prefix will be loaded from this path. + * @param $priority + */ + public static function addPath ($path, $prefix = '', $priority = 0) { - if (!isset ($_SESSION['tc'])) + if (substr ($path, -1) !== '/') + $path .= '/'; + + if ($prefix) { + $name = $prefix . '|' . $path; + } + else { + $name = $path; + } + + // Set priority + self::$pathpriorities[$name] = $priority; + + // Calculate the position based on priority. + $position = 0; + foreach (self::$paths as $path) { - $_SESSION['tc'] = time (); + if (self::$pathpriorities[$path] < $priority) + { + break; + } + $position ++; } - - $_SESSION['tc'] ++; - - return $_SESSION['tc']; + + array_splice (self::$paths, $position, 0, array ($name)); } - - private static function getTemplatePaths () + + /** + * @return string[] + */ + public static function getPaths () { - return explode (PATH_SEPARATOR, get_template_path ()); + return self::$paths; } - - // Text function - public function setTextSection ($sTextSection, $sTextFile = null) + + /** + * @param $sTextSection + * @param null $sTextFile + * @return $this + */ + private function setTextSection ($sTextSection, $sTextFile = null) { $this->sTextSection = $sTextSection; - + if (isset ($sTextFile)) { $this->sTextFile = $sTextFile; } + + return $this; } - - public function setTextFile ($sTextFile) + + /** + * @param $sTextFile + * @return $this + */ + private function setTextFile ($sTextFile) { $this->sTextFile = $sTextFile; + return $this; } + /** + * @param $var + * @param $value + * @param bool $overwrite + * @param bool $first + * @return $this + */ public function set ($var, $value, $overwrite = false, $first = false) { - $this->setVariable ($var, $value, $overwrite, $first); + $this->setVariable ($var, $value, $overwrite, $first, false); + return $this; } - - // Intern function - private function getText ($sKey, $sSection = null, $sFile = null, $sDefault = null) - { - if (!isset ($this->objText)) - { - $this->objText = Text::__getInstance (); - } - - $txt = Tools::output_varchar - ( - $this->objText->get - ( - $sKey, - isset ($sSection) ? $sSection : $this->sTextSection, - isset ($sFile) ? $sFile : $this->sTextFile, - false - ) - ); - - if (!$txt) - { - return $sDefault; - } - return $txt; + public function setRaw($var, $value, $overwrite = false, $first = false) + { + $this->setVariable ($var, $value, $overwrite, $first, true); + return $this; } - public function setVariable ($var, $value, $overwrite = false, $first = false) + /** + * @param $var + * @param $value + * @param bool $overwrite + * @param bool $first + */ + private function setVariable ($var, $value, $overwrite = false, $first = false, $raw = false) { - if ($overwrite) - { + if ($raw) { + $this->rawValues[$var] = true; + } + + if ($overwrite) { $this->values[$var] = $value; } - - else - { - if (isset ($this->values[$var])) - { - if ($first) - { + + else { + if (isset ($this->values[$var])) { + if ($first) { $this->values[$var] = $value.$this->values[$var]; } - - else - { + + else { $this->values[$var].= $value; } } - - else - { + + else { $this->values[$var] = $value; } } } - - public function addListValue ($var, $value) - { - $this->lists[$var][] = $value; - } - - public function putIntoText ($txt, $params = array ()) - { - return Tools::putIntoText ($txt, $params); - } - - public function sortList ($var) - { - if (isset ($this->lists[$var])) - { - sort ($this->lists[$var]); - } - } - - public function usortList ($var, $function) - { - if (isset ($this->lists[$var])) - { - usort ($this->lists[$var], $function); - } - } - - public function isTrue ($var) - { - return isset ($this->values[$var]) && $this->values[$var]; - } - - private static function getFilename ($template) + + /** + * Return an array of all filenames, or FALSE if none are found. + * @param $template + * @param bool $all + * @return bool|string + */ + private static function getFilenames ($template, $all = false) { - foreach (self::getTemplatePaths () as $v) - { + $out = array (); + + foreach (self::getPaths () as $v) { + // Split prefix and folder $split = explode ('|', $v); @@ -235,80 +236,271 @@ private static function getFilename ($template) if ($templatefixed == $prefix) { - $templaterest = substr ($template, strlen ($templatefixed)); - if (is_readable ($folder . '/' . $templaterest)) + if (is_readable ($folder . $templaterest)) { - return $folder . '/' . $templaterest; + $out[] = $folder . $templaterest; + + if (!$all) + return $out; } } } else { - if (is_readable ($v . '/' . $template)) + if (is_readable ($v . $template)) { - return $v . '/' . $template; + $out[] = $v . $template; + + if (!$all) + return $out; } } } - - return false; + + if (count ($out) > 0) + { + return $out; + } + return false; } - + public static function hasTemplate ($template) { - return self::getFilename ($template) != false; + return self::getFilenames ($template) ? true : false; } - - public function getClickTo ($sKey, $sSection = null, $sFile = null) + + public function parse ($template = null, $text = null) { - if (!isset ($this->objText)) + if (!isset ($template)) { - $this->objText = Text::__getInstance (); + if (isset ($this->template)) + { + $template = $this->template; + } + else { + throw new DataNotSet ("You must define a template name in constructor or as parse method parameter."); + } } - - return $this->objText->getClickTo ($this->getText ($sKey, $sSection, $sFile)); - } - public function parse ($template, $text = null) - { - /* Set static url adress */ - $this->set ('STATIC_URL', TEMPLATE_DIR); - - // SEt unique id - $this->set ('templateID', self::getUniqueId ()); + if (! $ctlbtmpltfiles = $this->getFilenames ($template)) + { + $out = '

Template not found

'; + $out .= '

The system could not find template "'.$template.'"

'; + return $out; + } ob_start (); - - if (! $filename = $this->getFilename ($template)) + + foreach (self::$shares as $k => $v) { - echo '

Template not found

'; - echo '

The system could not find template "'.$template.'"

'; - - $filename = null; + if (is_string($v) && !$this->isRaw($k)) { + ${$k} = htmlentities($v); + ${'_' . $k . '_'} = $v; + } else { + ${$k} = $v; + } } - - foreach ($this->values as $k => $v) - { - $$k = $v; + + foreach ($this->values as $k => $v) { + if (is_string($v) && !$this->isRaw($k)) { + ${$k} = htmlentities($v); + ${'_' . $k . '_'} = $v; + } else { + ${$k} = $v; + } } - - foreach ($this->lists as $k => $v) - { - $n = 'list_'.$k; - $$n = $v; + + include $ctlbtmpltfiles[0]; + + $val = ob_get_contents(); + + ob_end_clean(); + + return $this->processRenderQueue (array ('content' => $val)); + } + + private function isRaw($var) + { + return isset($this->rawValues[$var]); + } + + private function processRenderQueue ($contents = array ()) + { + if (isset ($this->layoutRender)) { + $template = new self (); + + // Set the variables that have been set here. + foreach ($this->values as $k => $v) { + if ($this->isRaw($k)) { + $template->setRaw($k, $v); + } else { + $template->set($k, $v); + } + } + + // And now set the content blocks. + // This might overwrite other sets. + foreach ($contents as $k => $v) { + $template->setRaw ($k, $v, true); + } + + return $template->parse ($this->layoutRender); } - - - if (isset ($filename)) - { - include $filename; + else { + return $contents['content']; + } + } + + /** + * Extend a parent theme. + * @param $layout + */ + private function layout ($layout) + { + $this->layoutRender = $layout; + } + + /** + * Go trough all set template directories and search for + * a specific template. Concat all of them. + * @param $template + * @param array $parameters + * @return false|string + */ + private function combine ($template, $parameters = []) + { + ob_start(); + + foreach (self::$shares as $k => $v) { + ${$k} = $v; } - + + foreach ($this->values as $k => $v) { + ${$k} = $v; + } + + foreach ($parameters as $k => $v) { + ${$k} = $v; + } + + if ($ctlbtmpltfiles = $this->getFilenames($template, true)) { + foreach ($ctlbtmpltfiles as $ctlbtmpltfile) { + include $ctlbtmpltfile; + } + } + $val = ob_get_contents(); ob_end_clean(); return $val; } + + /** + * Include a single template inside another template. + * @param $template + * @param array $parameters + * @return false|string + */ + private function template ($template, $parameters = []) + { + ob_start(); + + foreach (self::$shares as $k => $v) { + ${$k} = $v; + } + + foreach ($this->values as $k => $v) { + ${$k} = $v; + } + + foreach ($parameters as $k => $v) { + ${$k} = $v; + } + + if ($ctlbtmpltfiles = $this->getFilenames($template)) { + foreach ($ctlbtmpltfiles as $ctlbtmpltfile) { + include $ctlbtmpltfile; + } + } + + $val = ob_get_contents(); + ob_end_clean(); + + return $val; + } + + /** + * @param string $name + * @param string $method + * @return string + */ + private function help ($name, $method = 'helper') + { + $args = func_get_args (); + array_shift ($args); + array_shift ($args); + + if (isset (self::$helpers[$name])) + { + $call = array (self::$helpers[$name], $method); + if (is_callable ($call)) + { + $out = call_user_func_array ($call, $args); + if ($out instanceof Template) + { + return $out->parse (); + } + else { + return $out; + } + } + else { + return '

Method ' . $method . ' on helper ' . $name . ' is not callable.

'; + } + } + else { + return '

Could not find helper ' . $name . '

'; + } + } + + private function css ($path) + { + return ''; + } + + private function js ($path) + { + return ''; + } + + private function textdomain ($domain) + { + \Neuron\Tools\Text::getInstance ()->setDomain ($domain); + } + + private function gettext ($message1) + { + return \Neuron\Tools\Text::getInstance ()->getText ($message1); + } + + private function ngettext ($message1, $message2 = null, $n = null) + { + return \Neuron\Tools\Text::getInstance ()->getText ($message1, $message2, $n); + } + + /** + * @param $path + * @param array $params + * @param bool $normalize + * @param null $appurl + * @return string + */ + private function getURL ($path, array $params = null, $normalize = true, $appurl = null) { + return \Neuron\URLBuilder::getURL ($path, $params, $normalize, $appurl); + } + + public function __toString () { + return $this->parse (); + } } -?> diff --git a/src/Neuron/Core/Text.php b/src/Neuron/Core/Text.php deleted file mode 100644 index 85b6678..0000000 --- a/src/Neuron/Core/Text.php +++ /dev/null @@ -1,274 +0,0 @@ -backup = new NeuronCoreText ($language, $baseText, CATLAB_LANGUAGE_PATH); - } - - else if ($baseText && $baseText != $language) - { - $this->backup = new NeuronCoreText ($baseText, false); - } - - // Take text - if (isset ($language)) - { - $this->root_dir = $pathname.$language; - $this->tag = $language; - } - - else - { - echo 'Language directory not defined.'; - exit (); - } - - } - - public function setLanguage ($language) - { - $this->root_dir = LANGUAGE_DIR.$language; - $this->tag = $language; - - // Remove cache - $this->cache = array (); - } - - public function setFile ($file) - { - - $this->inFile = $file; - - } - - public function setSection ($section) - { - $this->inSection = $section; - } - - public function get ($id, $section = null, $file = null, $alternative = null) - { - throw new Deprecated ("Nope. Use gettext."); - - // Section & fill - if (empty ($section)) - { - $section = $this->inSection; - } - - if (empty ($file)) - { - $file = $this->inFile; - } - - // Check if the file is loaded already - if (empty ($this->cache[$file])) - { - $this->load_file ($file); - } - - // Check if the id exist - if (empty ($this->cache[$file][$section][$id])) - { - if ($this->backup && $this->backup->get ($id, $section, $file, false)) - { - return $this->backup->get ($id, $section, $file, false); - } - elseif ($alternative === null) - { - return 'Text Not Found: '.$id.' ('.$file.', '.$section.', '.$this->root_dir.')'; - } - else - { - return $alternative; - } - } - else { - return $this->cache[$file][$section][$id]; - } - } - - public function getSection ($section, $file) - { - - // Check if the file is loaded already - if (empty ($this->cache[$file])) - { - $this->load_file ($file); - } - - $output = array (); - - if (!empty ($this->cache[$file][$section])) - { - foreach ($this->cache[$file][$section] as $k => $v) - { - if (!empty ($v)) { - $output[$k] = $v; - } - } - } - - if (count ($output) == 0) - { - $output = $this->backup->getSection ($section, $file); - } - - return $output; - } - - public function getRandomLine ($section, $file, $iNumber = false) - { - $section = array_values ($this->getSection ($section, $file)); - - $total = count ($section); - - if ($total < 1) - { - return null; - } - - if (!$iNumber) - { - $iNumber = mt_rand (0, $total); - } - - return $section[$iNumber % $total]; - } - - public function getFile ($inFile, $return = 'error') - { - - $file = $this->root_dir.'/templates/'.$inFile.'.txt'; - - if (is_readable ($file)) - { - return file_get_contents ($file); - } - - elseif ($this->backup && $this->backup->getFile ($inFile, false)) - { - return $this->backup->getFile ($inFile); - } - - else { - if ($return === 'error') - { - return ('404: Unable to load file '.$file.'.'); - } - - else return $return; - } - } - - public function getTemplate ($file, $fields = array ()) - { - return Tools::putIntoText - ( - $this->getFile ($file), - $fields - ); - } - - protected function load_file ($file) - { - $f = $this->root_dir.'/'.$file.'.lng'; - - if (is_readable ($f)) - { - $this->cache[$file] = parse_ini_file ($f, true); - } - } - - /* - Return the abasolute path to the - text file. - */ - public function getPath () - { - return $this->root_dir . '/'; - } - - public function getClickto ($txt) - { - return Array - ( - $this->get ('clickto1', 'main', 'main').' ', - $this->get ('clickto2', 'main', 'main'), - ' '.$txt - ); - } - - public static function getLanguages () - { - $o = array (); - $dir = scandir (LANGUAGE_DIR); - foreach ($dir as $file) - { - if ($file != '.' && $file != '..' && strlen ($file) == 2) - { - $o[] = $file; - } - } - return $o; - } - - public function getCurrentLanguage () - { - return $this->tag; - } -} diff --git a/src/Neuron/Core/Tools.php b/src/Neuron/Core/Tools.php index b4540ea..8b7ce28 100755 --- a/src/Neuron/Core/Tools.php +++ b/src/Neuron/Core/Tools.php @@ -3,6 +3,7 @@ namespace Neuron\Core; +use DateTime; use Neuron\Core\Tools as NeuronCoreTools; use Exception; use Neuron\Core\Text; @@ -12,7 +13,7 @@ class Tools { - public static function getInput ($dat, $key, $type, $default = false) + public static function getInput ($dat, $key, $type, $default = null) { if (is_string ($dat)) { @@ -33,12 +34,20 @@ public static function getInput ($dat, $key, $type, $default = false) // For date's return timestamp. case 'date': $time = explode ('-', $dat[$key]); - return mktime (0, 0, 1, $time[1], $time[2], $time[0]); - break; + return mktime (0, 0, 1, (int)$time[1], (int)$time[2], (int)$time[0]); - default: + case 'datetime': + return new DateTime ($dat[$key]); + + case 'base64': + return base64_decode ($dat[$key]); + + case 'html': + case 'raw': return $dat[$key]; - break; + + default: + return strip_tags($dat[$key]); } } @@ -61,19 +70,27 @@ public static function checkInput ($value, $type) return true; } - else if ($type == 'bool') + elseif ($type == 'bool') { - return $value == 1 || $value == 'true'; + return $value === 1 || $value === '1' || $value === 'true'; } - elseif ($type == 'varchar') + elseif ($type == 'varchar' || $type == 'string' || $type == 'html') { return self::isValidUTF8 ($value); } + + elseif ($type === 'name') + { + return self::isValidUTF8 ($value) && strip_tags($value) === $value; + } elseif ($type == 'password') { - return self::isValidUTF8 ($value) && strlen ($value) > 2; + // Minimum 8 characters, maximum 256 characters. + return self::isValidUTF8 ($value) + && strlen ($value) > 7 + && strlen ($value) <= 256; } elseif ($type == 'email') @@ -89,8 +106,20 @@ public static function checkInput ($value, $type) elseif ($type == 'date') { - $time = explode ('-', $value); - return self::isValidUTF8 ($value) && (count ($time) == 3); + if (!self::isValidUTF8($value)) { + return false; + } + $time = explode('-', $value); + return count($time) === 3 + && ctype_digit($time[0]) + && ctype_digit($time[1]) + && ctype_digit($time[2]) + && checkdate((int)$time[1], (int)$time[2], (int)$time[0]); + } + + elseif ($type == 'datetime') { + $regex = '/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/'; + return (bool)preg_match ($regex, $value); } elseif ($type == 'md5') @@ -98,6 +127,10 @@ public static function checkInput ($value, $type) return self::isValidUTF8 ($value) && strlen ($value) == 32; } + elseif ($type == 'base64') { + return self::isValidBase64 ($value); + } + elseif ($type == 'url') { $regex = '/((https?:\/\/|[w]{3})?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)/i'; @@ -130,6 +163,11 @@ public static function checkInput ($value, $type) { return is_numeric ($value) && (int)$value == $value; } + + elseif ($type == 'raw') + { + return true; + } else { @@ -150,6 +188,11 @@ public static function isValidUTF8 ($str) return (bool) preg_match('//u', $str); } + public static function isValidBase64 ($str) + { + return base64_encode(base64_decode($str, true)) === $str; + } + public static function putIntoText ($text, $ar = array(), $delimiter = '@@') { foreach ($ar as $k => $v) @@ -239,132 +282,6 @@ public static function output_varchar ($text) return htmlspecialchars (($text), ENT_QUOTES, 'UTF-8'); } - - public static function writexml (XMLWriter $xml, $data, $item_name = 'item') - { - foreach($data as $key => $value) - { - if (is_int ($key)) - { - $key = $item_name; - } - - if (is_array($value)) - { - if ($key != 'items') - { - $xml->startElement($key); - } - - if (isset ($value['attributes']) && is_array ($value['attributes'])) - { - foreach ($value['attributes'] as $k => $v) - { - $xml->writeAttribute ($k, $v); - } - - unset ($value['attributes']); - } - - NeuronCoreTools::writexml ($xml, $value, substr ($key, 0, -1)); - - if ($key != 'items') - { - $xml->endElement(); - } - } - - elseif ($key == 'element-content') - { - $xml->text ($value); - } - - else - { - $xml->writeElement($key, $value); - } - } - } - - public static function output_xml ($data, $version = '0.1', $root = 'root', $parameters = array (), $sItemName = 'item') - { - $xml = new XmlWriter(); - $xml->openMemory(); - $xml->startDocument('1.0', 'UTF-8'); - $xml->startElement($root); - $xml->setIndent (true); - - if (!empty ($version)) - { - $xml->writeAttribute ('version', $version); - } - - foreach ($parameters as $paramk => $paramv) - { - $xml->writeAttribute ($paramk, $paramv); - } - - NeuronCoreTools::writexml ($xml, $data, $sItemName); - - $xml->endElement(); - return $xml->outputMemory(true); - } - - private static function xml_escape ($input) - { - //$input = str_replace ('"', '"', $input); - //$input = str_replace ("'", ''', $input); - - - $input = str_replace ('<', '<', $input); - $input = str_replace ('>', '>', $input); - $input = str_replace ('&', '&', $input); - - - return $input; - } - - public static function output_partly_xml ($data, $key = null) - { - $output = '<'.$key; - - if (isset ($data['attributes']) && is_array ($data['attributes'])) - { - foreach ($data['attributes'] as $k => $v) - { - $output .= ' '.$k.'="'.$v.'"'; - } - - unset ($data['attributes']); - } - - $output .= '>'; - if (!is_array ($data)) - { - $output .= self::xml_escape ($data); - } - - elseif (count ($data) == 1 && isset ($data['element-content'])) - { - $output .= self::xml_escape ($data['element-content']); - } - - else - { - foreach ($data as $k => $v) - { - if (is_numeric ($k)) - { - $k = substr ($key, 0, -1); - } - - $output .= self::output_partly_xml ($v, $k); - } - } - $output .= ''."\n"; - - return $output; - } } ?> diff --git a/src/Neuron/DB/Database.php b/src/Neuron/DB/Database.php index b9f0fb9..91d02dd 100755 --- a/src/Neuron/DB/Database.php +++ b/src/Neuron/DB/Database.php @@ -54,11 +54,26 @@ public function setLogger (Logger $logger) $this->logger = $logger; } + /** @var Database|null Test-only override, set via setInstance() */ + private static $testInstance = null; + + /** + * Override the singleton instance for testing purposes. + * @param Database|null $instance + */ + public static function setInstance (?Database $instance) + { + self::$testInstance = $instance; + } + /** * @return Database */ public static function getInstance () { + if (self::$testInstance !== null) { + return self::$testInstance; + } return self::__getInstance (); } @@ -67,19 +82,31 @@ public function getInsertId () return $this->insert_id; } + /** + * @return int + */ public function getAffectedRows () { return $this->affected_rows; } + /** + * @return int + */ public function getQueryCounter () { return $this->query_counter; } // Abstract functions - public abstract function query ($sSQL); - public abstract function multiQuery ($sSQL); + /** + * @param $sSQL + * @return Result|int + */ + public abstract function query($sSQL); + + public abstract function multiQuery($sSQL); + public abstract function escape ($txt); public function start () diff --git a/src/Neuron/DB/DatabaseSQLite.php b/src/Neuron/DB/DatabaseSQLite.php deleted file mode 100644 index b015dbd..0000000 --- a/src/Neuron/DB/DatabaseSQLite.php +++ /dev/null @@ -1,225 +0,0 @@ -logger = $logger; - } - - /** - * @return Database - */ - public static function getInstance () - { - return self::__getInstance (); - } - - public function getInsertId () - { - return $this->insert_id; - } - - public function getAffectedRows () - { - return $this->affected_rows; - } - - public function getQueryCounter () - { - return $this->query_counter; - } - - // Abstract functions - public abstract function query ($sSQL); - public abstract function multiQuery ($sSQL); - public abstract function escape ($txt); - - public function start () - { - $this->query ("START TRANSACTION"); - } - - public function commit () - { - $this->query ("COMMIT"); - } - - public function rollback () - { - $this->query ("ROLLBACK"); - } - - /** - * Just put a comment in the query log. - * Does not connect to database. - * @param $txt - */ - public function log ($txt) - { - $this->addQueryLog ("/* " . $txt . " */"); - } - - protected function addQueryLog ($sSQL, $duration = null) - { - - // SQL LOGGING :: BEGIN - if (defined ('DB_CUSTOM_LOG_SQLITE') && DB_CUSTOM_LOG_SQLITE) { - - $error_log_folder = str_replace('\\','/', dirname(__FILE__) .'/../../../../logs'); - $error_log_filename = "$error_log_folder/sqlite_log.txt"; - $error_log_extended_filename = "$error_log_folder/sqlite_log_extended.txt"; - - //$str_context = print_r($context, true); - - //$str_debug_backtrace = print_r(debug_backtrace(), true); - - $str_log = "-- ----------------------------------------------------------------------------\n$sSQL;\n"; - - file_put_contents($error_log_filename, $str_log, FILE_APPEND | LOCK_EX); - } - - // SQL LOGGING :: END - - - if (! (defined ('DEBUG') && DEBUG) && !isset ($_GET['debug'])) - { - return; - } - - if (count ($this->query_log) > 5000) - { - array_shift ($this->query_log); - } - - $stacktrace = debug_backtrace (); - $origin = $stacktrace[1]; - - //var_dump ($stacktrace[2]); - - if (isset ($stacktrace[2]['class']) && $stacktrace[2]['class'] == 'Neuron\DB\Query') - { - $origin = $stacktrace[2]; - } - - $txt = '[' . number_format ($duration, 3) . ' s] '; - //$txt .= $origin['class'] . ' '; - - // QUery took longer than 1 second? - /* - if ($duration > 0.1) - { - $txt = '' . $txt . ''; - } - */ - - $txt .= trim ($sSQL); - - $this->increaseOriginCounter ($origin['file'], $origin['line']); - //$txt .= '
' . $origin['file'] . ':' . $origin['line']; - - if (isset ($this->logger)) - { - $color = 'green'; - if (strpos ($txt, 'START') !== false || strpos ($txt, 'COMMIT') !== false || strpos ($txt, 'ROLLBACK') !== false) - { - $color = 'red'; - } - - $this->logger->log ('DB: ' . preg_replace('!\s+!', ' ', str_replace ("\t", " ", str_replace ("\n", "", $txt))), false, $color); - } - - $this->query_log[] = $txt; - } - - public function getLastQuery () - { - return $this->query_log[count ($this->query_log) - 1]; - } - - public function getAllQueries () - { - return $this->query_log; - } - - public function getConnection () - { - - } - - private function increaseOriginCounter ($file, $line) - { - if (!isset ($this->origin_counter[$file . ':' . $line])) - { - $this->origin_counter[$file . ':' . $line] = 1; - } - else - { - $this->origin_counter[$file . ':' . $line] ++; - } - } - - public function getOriginCounters () - { - arsort ($this->origin_counter, SORT_NUMERIC); - - $out = array (); - foreach ($this->origin_counter as $k => $v) - { - $out[] = 'Queries: ' . $v . ': ' . $k; - } - return $out; - } - - // Functions that should not be used... but well, we can't do without them at the moment - public abstract function fromUnixtime ($timestamp); - public abstract function toUnixtime ($date); -} -?> diff --git a/src/Neuron/DB/MySQL.php b/src/Neuron/DB/MySQL.php index 228a974..60723e9 100755 --- a/src/Neuron/DB/MySQL.php +++ b/src/Neuron/DB/MySQL.php @@ -1,6 +1,7 @@ connection = new MySQLi (DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME); - - $this->connection->query ('SET names "' . DB_CHARSET . '"'); + $this->connection = new MySQLi + ( + Config::get ('database.mysql.host'), + Config::get ('database.mysql.username'), + Config::get ('database.mysql.password'), + Config::get ('database.mysql.database') + ); + + $this->connection->query ('SET names "' . Config::get ('database.mysql.charset') . '"'); //$this->connection->query ("SET time_zone = '+00:00'"); } catch (Exception $e) @@ -141,19 +148,10 @@ public function query ($sSQL, $log = true) $this->addQueryLog($sSQL, $duration); } - if (!$result) - { - //var_dump (debug_backtrace ()); - //$data = debug_backtrace (); - //print_r ($data); - - - echo $sSQL; - throw new DbException ('MySQL Error: '.$this->connection->error); - } - - elseif ($result instanceof MySQLi_Result) - { + if (!$result) { + throw (new DbException ('MySQL Error: '.$this->connection->error)) + ->setErrorCode($this->connection->errno); + } elseif ($result instanceof MySQLi_Result) { return new Result ($result); } diff --git a/src/Neuron/DB/Query.php b/src/Neuron/DB/Query.php index 4d6832c..5c9693e 100644 --- a/src/Neuron/DB/Query.php +++ b/src/Neuron/DB/Query.php @@ -1,22 +1,23 @@ $v) - { + $query = 'INSERT INTO ' . self::escapeTableName($table) . ' SET '; + $values = array (); + foreach ($set as $k => $v) { $query .= $k . ' = ?, '; // No array? Then it's a simple string. - if (is_array ($v)) - { + if (is_array ($v)) { $values[] = $v; - } - else - { - $values[] = array ($v); + } else { + $values[] = array($v); } } - $query = substr ($query, 0, -2); + $query = substr($query, 0, -2); - $query = new self ($query); - $query->bindValues ($values); + $query = new self($query); + $query->bindValues($values); return $query; } /** * Generate an replace query - * @param $table: table to insert data to - * @param $set: a 2 dimensional array with syntax: { column_name : [ value, type, nullOnEmpty ]} + * @param string $table: table to insert data to + * @param mixed[] $set: a 2 dimensional array with syntax: { column_name : [ value, type, nullOnEmpty ]} * @return Query */ - public static function replace ($table, array $set) + public static function replace($table, array $set) { - $query = 'REPLACE INTO `' . $table . '` SET '; + $query = 'REPLACE INTO ' . self::escapeTableName($table) . ' SET '; $values = array (); foreach ($set as $k => $v) { $query .= $k . ' = ?, '; // No array? Then it's a simple string. - if (is_array ($v)) + if (is_array($v)) { $values[] = $v; } else { - $values[] = array ($v); + $values[] = array($v); } } - $query = substr ($query, 0, -2); + $query = substr($query, 0, -2); - $query = new self ($query); - $query->bindValues ($values); + $query = new self($query); + $query->bindValues($values); return $query; } @@ -97,105 +94,86 @@ public static function replace ($table, array $set) * nullOnEmpty may be omitted. * @return Query */ - public static function update ($table, array $set, array $where) + public static function update($table, array $set, array $where) { - $query = 'UPDATE `' . $table . '` SET '; - $values = array (); - foreach ($set as $k => $v) - { + $query = 'UPDATE ' . self::escapeTableName($table) . ' SET '; + $values = array(); + foreach ($set as $k => $v) { $query .= $k . ' = ?, '; // No array? Then it's a simple string. - if (is_array ($v)) - { + if (is_array($v)) { $values[] = $v; - } - else - { - $values[] = array ($v); + } else { + $values[] = array($v); } } - $query = substr ($query, 0, -2) . ' '; - $query .= self::processWhere ($where, $values); + $query = substr($query, 0, -2) . ' '; + $query .= self::processWhere($where, $values); - $query = new self ($query); - $query->bindValues ($values); + $query = new self($query); + $query->bindValues($values); return $query; } - private static function processWhere (array $where, &$values) + /** + * @param mixed[] $where + * @param mixed[] $values + * @return string + */ + private static function processWhere(array $where, &$values) { $query = ''; - if (count ($where) > 0) - { + if (count($where) > 0) { $query .= 'WHERE '; - foreach ($where as $k => $v) - { + foreach ($where as $k => $v) { // No array? Then it's a simple string. - if (is_array ($v)) - { + if (is_array($v)) { $tmp = $v; - } - else - { - $tmp = array ($v, self::PARAM_STR); + } else { + $tmp = array($v, self::PARAM_UNKNOWN); } - if (!is_array ($tmp[0]) && substr ($tmp[0], 0, 1) == '!') - { + // Parse comparators + if (!is_array($tmp[0]) && substr($tmp[0], 0, 1) === '!') { $query .= $k . ' != ? AND '; - $tmp[0] = substr ($tmp[0], 1); - } - - else if (isset ($tmp[2]) && strtoupper ($tmp[2]) == 'LIKE') - { + $tmp[0] = substr($tmp[0], 1); + } elseif (isset($tmp[2]) && strtoupper($tmp[2]) === 'LIKE') { $query .= $k . ' LIKE ? AND '; - $tmp = array ($tmp[0], $tmp[1]); - } - - else if (isset ($tmp[2]) && strtoupper ($tmp[2]) == 'NOT') - { + $tmp = array($tmp[0], $tmp[1]); + } elseif (isset ($tmp[2]) && strtoupper($tmp[2]) === 'NOT') { $query .= $k . ' != ? AND '; - $tmp = array ($tmp[0], $tmp[1]); - } - - else if (isset ($tmp[2]) + $tmp = array($tmp[0], $tmp[1]); + } elseif ( + isset ($tmp[2]) && ( - strtoupper ($tmp[2]) == '>' - || strtoupper ($tmp[2]) == '<' - || strtoupper ($tmp[2]) == '>=' - || strtoupper ($tmp[2]) == '<=' - || strtoupper ($tmp[2]) == '!=' + strtoupper ($tmp[2]) === '>' + || strtoupper ($tmp[2]) === '<' + || strtoupper ($tmp[2]) === '>=' + || strtoupper ($tmp[2]) === '<=' + || strtoupper ($tmp[2]) === '!=' ) - ) - { + ) { $query .= $k . ' ' . $tmp[2] . ' ? AND '; - $tmp = array ($tmp[0], $tmp[1]); - } - - else if (isset ($tmp[2]) && strtoupper ($tmp[2]) == 'IN') - { + $tmp = array($tmp[0], $tmp[1]); + } elseif (isset($tmp[2]) && strtoupper($tmp[2]) == 'IN') { $query .= $k . ' ' . $tmp[2] . ' ? AND '; $tmp = array ($tmp[0], $tmp[1]); - } - - else if (is_array ($tmp[0])) - { + } elseif (is_array($tmp[0])) { $query .= $k . ' IN ? AND '; - } - - else - { + } elseif ($tmp[0] === null) { + $query .= $k . ' IS NULL AND '; + } else { $query .= $k . ' = ? AND '; } $values[] = $tmp; } - $query = substr ($query, 0, -5); + $query = substr($query, 0, -5); } return $query; @@ -210,46 +188,44 @@ private static function processWhere (array $where, &$values) * @param null $limit * @return Query */ - public static function select ($table, array $data = array (), array $where = array (), $order = array (), $limit = null) - { + public static function select ( + $table, + array $data = array(), + array $where = array(), + $order = array(), + $limit = null + ) { $query = 'SELECT '; $values = array (); - if (count ($data) > 0) - { - foreach ($data as $v) - { + if (count ($data) > 0) { + foreach ($data as $v) { $query .= $v . ', '; } $query = substr ($query, 0, -2) . ' '; - } - else - { + } else { $query .= '* '; } - $query .= 'FROM `' . $table . '` '; + $query .= 'FROM ' . self::escapeTableName($table) . ' '; $query .= self::processWhere ($where, $values); // Order - if (count ($order) > 0) - { + if (count ($order) > 0) { $query .= " ORDER BY "; - foreach ($order as $v) - { + foreach ($order as $v) { $query .= $v . ", "; } $query = substr ($query, 0, -2); } // Limit - if ($limit) - { + if ($limit) { $query .= " LIMIT " . $limit; } - $query = new self ($query); - $query->bindValues ($values); + $query = new self($query); + $query->bindValues($values); return $query; } @@ -259,187 +235,102 @@ public static function select ($table, array $data = array (), array $where = ar * @param array $where * @return Query|string */ - public static function delete ($table, array $where) + public static function delete($table, array $where) { - $query = 'DELETE FROM `' . $table . '`'; + $query = 'DELETE FROM ' . self::escapeTableName($table) . ''; - $values = array (); - $query .= self::processWhere ($where, $values); + $values = array(); + $query .= self::processWhere($where, $values); - $query = new self ($query); - $query->bindValues ($values); + $query = new self($query); + $query->bindValues($values); return $query; } /** - * And construct. - */ + * And construct. + */ public function __construct ($query) { $this->query = $query; } + /** + * @param $values + * @return Query + */ public function bindValues ($values) { $this->values = $values; + return $this; } - public function bindValue ($index, $value, $type = self::PARAM_STR, $canBeNull = false) - { + /** + * @param string $index + * @param mixed $value + * @param int $type + * @param bool $canBeNull + * @return Query + */ + public function bindValue( + $index, + $value, + $type = self::PARAM_UNKNOWN, + $canBeNull = false + ) { $this->values[$index] = array ($value, $type, $canBeNull); // Chaining return $this; } + /** + * @return string + */ public function getParsedQuery () { - $db = Database::getInstance (); - - $keys = array (); - $values = array (); + $keys = array(); + $values = array(); - foreach ($this->values as $k => $v) - { + foreach ($this->values as $k => $v) { // Column type? - if (!isset ($v[1])) - { + if (!isset ($v[1])) { // Check for known "special types" - if ($v[0] instanceof Point) - { + if ($v[0] instanceof Point) { $v[1] = self::PARAM_POINT; - } - else - { - $v[1] = self::PARAM_STR; + } elseif ($v[0] instanceof DateTime) { + $v[1] = self::PARAM_DATE; + } else { + $v[1] = self::PARAM_UNKNOWN; } } // NULL on empty? - if (!isset ($v[2])) - { - $v[2] = false; + if (!isset($v[2])) { + $v[2] = true; } - // Empty and should set NULL? - if ($v[2] && empty ($v[0])) - { - $value = "NULL"; - } - else - { - // Array? - if (is_array ($v[0])) - { - switch ($v[1]) - { - case self::PARAM_NUMBER: - foreach ($v[0] as $kk => $vv) - { - if (!is_numeric ($vv)) - { - throw new InvalidParameter ("Parameter " . $k . "[" . $kk . "] should be numeric in query " . $this->query); - } - } - $value = '(' . implode (',', $v[0]) . ')'; - break; - - case self::PARAM_DATE: - - $tmp = array (); - foreach ($v[0] as $kk => $vv) - { - if (!is_numeric ($vv)) - { - throw new InvalidParameter ("Parameter " . $k . "[" . $kk . "] should be a valid timestamp in query " . $this->query); - } - $tmp[] = "FROM_UNIXTIME(" . $vv . ")"; - } - $value = '(' . implode (',', $tmp) . ')'; - - break; - - case self::PARAM_POINT: - $tmp = array (); - foreach ($v[0] as $kk => $vv) - { - if (! ($vv instanceof Point)) - { - throw new InvalidParameter ("Parameter " . $k . "[" . $kk . "] should be a valid \\Neuron\\Models\\Point " . $this->query); - } - $tmp[] = "POINT(" . $vv->getLongtitude() . "," . $vv->getLatitude() .")"; - } - $value = '(' . implode (',', $tmp) . ')'; - break; - - case self::PARAM_STR: - default: - $tmp = array (); - foreach ($v[0] as $kk => $vv) - { - $tmp[] = "'" . $db->escape (strval ($vv)) . "'"; - } - $value = '(' . implode (',', $tmp) . ')'; - break; - } - } - else - { - switch ($v[1]) - { - case self::PARAM_NUMBER: - if (!is_numeric ($v[0])) - { - throw new InvalidParameter ("Parameter " . $k . " should be numeric in query " . $this->query); - } - $value = $v[0]; - break; - - case self::PARAM_DATE: - if (!is_numeric ($v[0])) - { - throw new InvalidParameter ("Parameter " . $k . " should be a valid timestamp in query " . $this->query); - } - $value = "FROM_UNIXTIME(" . $v[0] . ")"; - break; - - case self::PARAM_POINT: - if (! ($v[0] instanceof Point)) - { - throw new InvalidParameter ("Parameter " . $k . " should be a valid \\Neuron\\Models\\Point " . $this->query); - } - else - { - $value = "POINT(" . $v[0]->getLongtitude() . "," . $v[0]->getLatitude() .")"; - } - break; - - case self::PARAM_STR: - default: - $value = "'" . $db->escape (strval ($v[0])) . "'"; - break; - } - } - } + // Empty and should set NULL? + if ($v[2] && $v[0] === null) { + $value = "NULL"; + } else { + $value = $this->getValues($k, $v); + } $values[$k] = $value; // Replace question marks or tokens? - if (is_string ($k)) - { + if (is_string ($k)) { $keys[] = '/:'.$k.'/'; - } - else - { + } else { $keys[] = '/[?]/'; } } // First we make a list with placeholders which we will later repalce with values $fakeValues = array (); - foreach ($values as $k => $v) - { + foreach ($values as $k => $v) { $fakeValues[$k] = '{{{ctlb-custom-placeholder-' . $k . '}}}'; } @@ -447,19 +338,106 @@ public function getParsedQuery () $query = preg_replace ($keys, $fakeValues, $this->query, 1); // And now replace the tokens with the actual values - foreach ($values as $k => $v) - { + foreach ($values as $k => $v) { $query = str_replace ($fakeValues[$k], $v, $query); } return $query; } + /** + * @param mixed $value + * @param string $type + * @param string $parameterName + * @return int|string + * @throws InvalidParameter + */ + private function getValue ($value, $type, $parameterName) + { + $db = Database::getInstance (); + + switch ($type) { + case self::PARAM_NUMBER: + if (!is_numeric ($value)) { + throw new InvalidParameter ("Parameter " . $parameterName . " should be numeric in query " . $this->query); + } + return (string)str_replace (',', '.', $value); + + case self::PARAM_DATE: + + if ($value instanceof DateTime) { + return "'" . $value->format ('Y-m-d H:i:s') . "'"; + } + + else if (is_numeric ($value)) { + return "FROM_UNIXTIME(" . $value . ")"; + } + else { + throw new InvalidParameter ("Parameter " . $parameterName . " should be a valid timestamp in query " . $this->query); + } + + case self::PARAM_POINT: + if (! ($value instanceof Point)) + { + throw new InvalidParameter ("Parameter " . $parameterName . " should be a valid \\Neuron\\Models\\Point " . $this->query); + } + return $value = "POINT(" . $value->getLongtitude() . "," . $value->getLatitude() .")"; + + case self::PARAM_STR: + if (is_numeric ($value)) { + $value = (string)str_replace (',', '.', $value); + } + + return "'" . $db->escape (strval ($value)) . "'"; + + case self::PARAM_UNKNOWN: + if (is_int($value)) { + return intval($value); + } elseif (is_numeric ($value)) { + $value = (string)str_replace (',', '.', $value); + } + return "'" . $db->escape (strval ($value)) . "'"; + + } + } + + /** + * @return Result|int + */ public function execute () { - $db = Database::getInstance (); - $query = $this->getParsedQuery (); - return $db->query ($query); + $db = Database::getInstance(); + $query = $this->getParsedQuery(); + return $db->query($query); } -} -?> + + /** + * @param $k + * @param $v + * @return string + * @throws InvalidParameter + */ + private function getValues ($k, $v) + { + if (is_array ($v[0])) { + $tmp = array (); + + foreach ($v[0] as $kk => $vv) { + $tmp[] = $this->getValue($vv, $v[1], $k . '[' . $kk . ']'); + } + + return '(' . implode (',', $tmp) . ')'; + } else { + return $this->getValue($v[0], $v[1], $k); + } + } + + /** + * @param string $table + * @return string + */ + private static function escapeTableName($table) + { + return "`$table`"; + } +} \ No newline at end of file diff --git a/src/Neuron/DB/QuerySQLite.php b/src/Neuron/DB/QuerySQLite.php deleted file mode 100644 index a7ca588..0000000 --- a/src/Neuron/DB/QuerySQLite.php +++ /dev/null @@ -1,499 +0,0 @@ - $v) - { - $str_columns .= "$k,"; - - //TODO: Prepare binValues with SQLite3 - //$str_values .= $k . ' = ?, '; - - $str_values .= '\''. $v .'\', '; - - // No array? Then it's a simple string. - if (is_array ($v)) - { - $values[] = $v; - } - else - { - $values[] = array ($v); - } - } - - $str_columns = substr($str_columns, 0, -1); - - $str_values = substr($str_values, 0, -2); - - $query = "$query ( $str_columns ) VALUES ( $str_values )"; - - $query = new self ($query); - //$query->bindValues ($values); - - return $query; - } - - /** - * Generate an replace query - * @param $table: table to insert data to - * @param $set: a 2 dimensional array with syntax: { column_name : [ value, type, nullOnEmpty ]} - * @return Query - */ - public static function replace ($table, array $set) - { - $query = 'REPLACE INTO `' . $table . '` SET '; - $values = array (); - foreach ($set as $k => $v) - { - $query .= $k . ' = ?, '; - - // No array? Then it's a simple string. - if (is_array ($v)) - { - $values[] = $v; - } - else - { - $values[] = array ($v); - } - } - - $query = substr ($query, 0, -2); - - $query = new self ($query); - $query->bindValues ($values); - - return $query; - } - - /** - * Generate an insert query - * @param $table: table to insert data to - * @param $set: a 2 dimensional array with syntax: { column_name : [ value, type, nullOnEmpty ]} - * @param $where: a 2 dimensional array with syntax: { column_name : [ value, type, nullOnEmpty ]} - * nullOnEmpty may be omitted. - * @return Query - */ - public static function update ($table, array $set, array $where) - { - $query = 'UPDATE `' . $table . '` SET '; - $values = array (); - foreach ($set as $k => $v) - { - $query .= $k . ' = ?, '; - - // No array? Then it's a simple string. - if (is_array ($v)) - { - $values[] = $v; - } - else - { - $values[] = array ($v); - } - } - - $query = substr ($query, 0, -2) . ' '; - $query .= self::processWhere ($where, $values); - - $query = new self ($query); - $query->bindValues ($values); - - return $query; - } - - private static function processWhere (array $where, &$values) - { - $query = ''; - - if (count ($where) > 0) - { - $query .= 'WHERE '; - foreach ($where as $k => $v) - { - // No array? Then it's a simple string. - if (is_array ($v)) - { - $tmp = $v; - } - else - { - $tmp = array ($v, self::PARAM_STR); - } - - if (!is_array ($tmp[0]) && substr ($tmp[0], 0, 1) == '!') - { - $query .= $k . ' != ? AND '; - $tmp[0] = substr ($tmp[0], 1); - } - - else if (isset ($tmp[2]) && strtoupper ($tmp[2]) == 'LIKE') - { - $query .= $k . ' LIKE ? AND '; - $tmp = array ($tmp[0], $tmp[1]); - } - - else if (isset ($tmp[2]) && strtoupper ($tmp[2]) == 'NOT') - { - $query .= $k . ' != ? AND '; - $tmp = array ($tmp[0], $tmp[1]); - } - - else if (isset ($tmp[2]) - && ( - strtoupper ($tmp[2]) == '>' - || strtoupper ($tmp[2]) == '<' - || strtoupper ($tmp[2]) == '>=' - || strtoupper ($tmp[2]) == '<=' - || strtoupper ($tmp[2]) == '!=' - ) - ) - { - $query .= $k . ' ' . $tmp[2] . ' ? AND '; - $tmp = array ($tmp[0], $tmp[1]); - } - - else if (isset ($tmp[2]) && strtoupper ($tmp[2]) == 'IN') - { - $query .= $k . ' ' . $tmp[2] . ' ? AND '; - $tmp = array ($tmp[0], $tmp[1]); - } - - else if (is_array ($tmp[0])) - { - $query .= $k . ' IN ? AND '; - } - - else - { - $query .= $k . ' = ? AND '; - } - - $values[] = $tmp; - } - - $query = substr ($query, 0, -5); - } - - return $query; - } - - /** - * Select data from a message - * @param $table - * @param array $data : array of column names [ column1, column2 ] - * @param array $where : a 2 dimensional array with syntax: { column_name : [ value, type, nullOnEmpty ]} - * @param array $order - * @param null $limit - * @return Query - */ - public static function select ($table, array $data = array (), array $where = array (), $order = array (), $limit = null) - { - $query = 'SELECT '; - $values = array (); - - if (count ($data) > 0) - { - foreach ($data as $v) - { - $query .= $v . ', '; - } - $query = substr ($query, 0, -2) . ' '; - } - else - { - $query .= '* '; - } - - $query .= 'FROM `' . $table . '` '; - $query .= self::processWhere ($where, $values); - - // Order - if (count ($order) > 0) - { - $query .= " ORDER BY "; - foreach ($order as $v) - { - $query .= $v . ", "; - } - $query = substr ($query, 0, -2); - } - - // Limit - if ($limit) - { - $query .= " LIMIT " . $limit; - } - - $query = new self ($query); - $query->bindValues ($values); - - return $query; - } - - /** - * @param $table - * @param array $where - * @return Query|string - */ - public static function delete ($table, array $where) - { - $query = 'DELETE FROM `' . $table . '`'; - - $values = array (); - $query .= self::processWhere ($where, $values); - - $query = new self ($query); - $query->bindValues ($values); - - return $query; - } - - /** - * And construct. - */ - public function __construct ($query) - { - $this->query = $query; - } - - public function bindValues ($values) - { - $this->values = $values; - } - - public function bindValue ($index, $value, $type = self::PARAM_STR, $canBeNull = false) - { - $this->values[$index] = array ($value, $type, $canBeNull); - - // Chaining - return $this; - } - - public function getParsedQuery () - { - $db = DatabaseSQLite::getInstance (); - - $keys = array (); - $values = array (); - - foreach ($this->values as $k => $v) - { - // Column type? - if (!isset ($v[1])) - { - // Check for known "special types" - if ($v[0] instanceof Point) - { - $v[1] = self::PARAM_POINT; - } - else - { - $v[1] = self::PARAM_STR; - } - } - - // NULL on empty? - if (!isset ($v[2])) - { - $v[2] = false; - } - - // Empty and should set NULL? - if ($v[2] && empty ($v[0])) - { - $value = "NULL"; - } - else - { - // Array? - if (is_array ($v[0])) - { - switch ($v[1]) - { - case self::PARAM_NUMBER: - foreach ($v[0] as $kk => $vv) - { - if (!is_numeric ($vv)) - { - throw new InvalidParameter ("Parameter " . $k . "[" . $kk . "] should be numeric in query " . $this->query); - } - } - $value = '(' . implode (',', $v[0]) . ')'; - break; - - case self::PARAM_DATE: - - $tmp = array (); - foreach ($v[0] as $kk => $vv) - { - if (!is_numeric ($vv)) - { - throw new InvalidParameter ("Parameter " . $k . "[" . $kk . "] should be a valid timestamp in query " . $this->query); - } - $tmp[] = "FROM_UNIXTIME(" . $vv . ")"; - } - $value = '(' . implode (',', $tmp) . ')'; - - break; - - case self::PARAM_POINT: - $tmp = array (); - foreach ($v[0] as $kk => $vv) - { - if (! ($vv instanceof Point)) - { - throw new InvalidParameter ("Parameter " . $k . "[" . $kk . "] should be a valid \\Neuron\\Models\\Point " . $this->query); - } - $tmp[] = "POINT(" . $vv->getLongtitude() . "," . $vv->getLatitude() .")"; - } - $value = '(' . implode (',', $tmp) . ')'; - break; - - case self::PARAM_STR: - default: - $tmp = array (); - foreach ($v[0] as $kk => $vv) - { - $tmp[] = "'" . $db->escape (strval ($vv)) . "'"; - } - $value = '(' . implode (',', $tmp) . ')'; - break; - } - } - else - { - switch ($v[1]) - { - case self::PARAM_NUMBER: - if (!is_numeric ($v[0])) - { - throw new InvalidParameter ("Parameter " . $k . " should be numeric in query " . $this->query); - } - $value = $v[0]; - break; - - case self::PARAM_DATE: - if (!is_numeric ($v[0])) - { - throw new InvalidParameter ("Parameter " . $k . " should be a valid timestamp in query " . $this->query); - } - $value = "FROM_UNIXTIME(" . $v[0] . ")"; - break; - - case self::PARAM_POINT: - if (! ($v[0] instanceof Point)) - { - throw new InvalidParameter ("Parameter " . $k . " should be a valid \\Neuron\\Models\\Point " . $this->query); - } - else - { - $value = "POINT(" . $v[0]->getLongtitude() . "," . $v[0]->getLatitude() .")"; - } - break; - - case self::PARAM_STR: - default: - $value = "'" . $db->escape (strval ($v[0])) . "'"; - break; - } - } - } - - $values[$k] = $value; - - // Replace question marks or tokens? - if (is_string ($k)) - { - $keys[] = '/:'.$k.'/'; - } - else - { - $keys[] = '/[?]/'; - } - } - - // First we make a list with placeholders which we will later repalce with values - $fakeValues = array (); - foreach ($values as $k => $v) - { - $fakeValues[$k] = '{{{ctlb-custom-placeholder-' . $k . '}}}'; - } - - // And replace - $query = preg_replace ($keys, $fakeValues, $this->query, 1); - - // And now replace the tokens with the actual values - foreach ($values as $k => $v) - { - $query = str_replace ($fakeValues[$k], $v, $query); - } - - return $query; - } - - public function execute () - { - $db = DatabaseSQLite::getInstance (); - $query = $this->getParsedQuery (); - - // SQL LOGGING :: BEGIN - if (defined ('DB_CUSTOM_LOG_SQLITE') && DB_CUSTOM_LOG_SQLITE) { - - $sSQL = $query; - - $error_log_folder = str_replace('\\','/', dirname(__FILE__) .'/../../../../logs'); - $error_log_filename = "$error_log_folder/sqlite_log.txt"; - $error_log_extended_filename = "$error_log_folder/sqlite_log_extended.txt"; - - //$str_context = print_r($context, true); - - //$str_debug_backtrace = print_r(debug_backtrace(), true); - - $str_log = "-- ----------------------------------------------------------------------------\n$sSQL;\n"; - - file_put_contents($error_log_filename, $str_log, FILE_APPEND | LOCK_EX); - } - - // SQL LOGGING :: END - - return $db->query ($query); - } -} -?> diff --git a/src/Neuron/DB/Result.php b/src/Neuron/DB/Result.php index 9e1893b..84eb85a 100755 --- a/src/Neuron/DB/Result.php +++ b/src/Neuron/DB/Result.php @@ -1,13 +1,15 @@ connection)) - { - try - { - - $this->connection = new \PDO(DB_OAUTH2_DSN); // success - - } - catch (Exception $e) - { - echo $e; - } - - /* - if (mysqli_connect_errno ()) - { - printf ("Connect failed: %s\n", mysqli_connect_error()); - exit(); - } - */ - } - } - - public function disconnect () - { - Logger::getInstance ()->log ('Disconnecting database.'); - if (isset ($this->connection)) - { - $this->connection->close (); - } - $this->connection = null; - } - - public function getConnection () - { - return $this->connection; - } - - public function multiQuery ($sSQL) - { - $start = microtime (true); - - $this->connect (); - - // Increase the counter - $this->query_counter ++; - - $result = $this->connection->multi_query (trim ($sSQL)); - - // FLUSH RESULTS - // @TODO make these usable - do { - $r = $this->connection->store_result (); - if ($r) - { - $r->free (); - } - - if (!$this->connection->more_results ()) - { - break; - } - - //$this->connection->next_result(); - } while ($this->connection->next_result ()); - - $duration = microtime (true) - $start; - $this->addQueryLog ($sSQL, $duration); - - if ($result === false) - { - //var_dump (debug_backtrace ()); - //$data = debug_backtrace (); - //print_r ($data); - - - echo $sSQL; - throw new DbException ('SQLite Error: '.$this->connection->error); - } - - elseif ($result instanceof MySQLi_Result) - { - return new Result ($result); - } - - // Insert ID will return zero if this query was not insert or update. - $this->insert_id = intval ($this->connection->insert_id); - - // Affected rows - $this->affected_rows = intval ($this->connection->affected_rows); - - if ($this->insert_id > 0) - return $this->insert_id; - - if ($this->affected_rows > 0) - return $this->affected_rows; - - return $result; - } - - /* - Execute a query and return a result - */ - public function query ($sSQL) - { - $start = microtime (true); - - $this->connect (); - - // Increase the counter - $this->query_counter ++; - - //$result = $this->connection->query (trim ($sSQL)); - - $result = $this->connection->exec(trim($sSQL)); - - $duration = microtime (true) - $start; - $this->addQueryLog ($sSQL, $duration); - - if ($result === false) - { - //var_dump (debug_backtrace ()); - //$data = debug_backtrace (); - //print_r ($data); - - - echo $sSQL; - - throw new DbException ('SQLite Error: '. implode("\n",$this->connection->errorInfo())); - - //throw new DbException ('SQLite Error: '.$this->connection->error); - } - - elseif ($result instanceof \SQLite3Result) - { - return new Result ($result); - } - - // Insert ID will return zero if this query was not insert or update. - $this->insert_id = intval ($this->connection->lastInsertId()); - - // Affected rows - //$this->affected_rows = intval ($this->connection->rowCount()); - - $this->affected_rows = $result; - - - if ($this->insert_id > 0) - return $this->insert_id; - - if ($this->affected_rows > 0) - return $this->affected_rows; - - return $result; - } - - public function escape ($txt) - { - if (is_array ($txt)) - { - throw new Error ('Invalid parameter: escape cannot handle arrays.'); - } - $this->connect (); - - // TODO: SQLite escape string - //return $this->connection->real_escape_string ($txt); - - return $txt; - } - - public function fromUnixtime ($timestamp) - { - $query = $this->query ("SELECT FROM_UNIXTIME('{$timestamp}') AS datum"); - return $query[0]['datum']; - } - - public function toUnixtime ($date) - { - $query = $this->query ("SELECT UNIX_TIMESTAMP('{$date}') AS datum"); - return $query[0]['datum']; - } -} -?> diff --git a/src/Neuron/Environment.php b/src/Neuron/Environment.php deleted file mode 100644 index eb4ddc0..0000000 --- a/src/Neuron/Environment.php +++ /dev/null @@ -1,25 +0,0 @@ -disconnect (); - \Neuron\Session::clearInstance (); - \Neuron\FrontController::destroy (); - } - -} \ No newline at end of file diff --git a/src/Neuron/EventManager.php b/src/Neuron/EventManager.php deleted file mode 100644 index 580176c..0000000 --- a/src/Neuron/EventManager.php +++ /dev/null @@ -1,72 +0,0 @@ -events)) - { - $args = func_get_args (); - foreach ($in->events as $method) - { - call_user_func_array ($method, $args); - } - } - } - - public static function on ($event, $callback) - { - if (!is_callable ($callback)) - { - throw new InvalidParameter ("All callbacks of events must be callables."); - } - - $in = self::getInstance (); - if (!isset ($in->triggers[$event])) - { - $in->triggers[$event] = array (); - } - $in->triggers[$event] = $callback; - } - - public static function off ($event) - { - $in = self::getInstance (); - $in->triggers[$event] = array (); - } - -} \ No newline at end of file diff --git a/src/Neuron/Exceptions/DataNotFound.php b/src/Neuron/Exceptions/DataNotFound.php index 5607a1b..d53e61c 100644 --- a/src/Neuron/Exceptions/DataNotFound.php +++ b/src/Neuron/Exceptions/DataNotFound.php @@ -3,11 +3,8 @@ namespace Neuron\Exceptions; -use Neuron\Core\Error; - - class DataNotFound - extends Error + extends \Exception { } diff --git a/src/Neuron/Exceptions/DataNotSet.php b/src/Neuron/Exceptions/DataNotSet.php index f39474d..841e271 100644 --- a/src/Neuron/Exceptions/DataNotSet.php +++ b/src/Neuron/Exceptions/DataNotSet.php @@ -3,11 +3,9 @@ namespace Neuron\Exceptions; -use Neuron\Core\Error; - class DataNotSet - extends Error + extends \Exception { } diff --git a/src/Neuron/Exceptions/DbException.php b/src/Neuron/Exceptions/DbException.php index 702440c..f819fa9 100644 --- a/src/Neuron/Exceptions/DbException.php +++ b/src/Neuron/Exceptions/DbException.php @@ -3,14 +3,14 @@ namespace Neuron\Exceptions; -use Neuron\Core\Error; - class DbException - extends Error + extends \Exception { private $query; + private $mysqlErrorCode; + /** * @param string $query */ @@ -18,5 +18,22 @@ public function setQuery ($query) { $this->query = $query; } + + /** + * @param $status + * @return $this + */ + public function setErrorCode($status) + { + $this->mysqlErrorCode = $status; + return $this; + } + + /** + * @return mixed + */ + public function getErrorCode() + { + return $this->mysqlErrorCode; + } } -?> diff --git a/src/Neuron/Exceptions/Deprecated.php b/src/Neuron/Exceptions/Deprecated.php index 442759c..5e1ef7e 100644 --- a/src/Neuron/Exceptions/Deprecated.php +++ b/src/Neuron/Exceptions/Deprecated.php @@ -3,15 +3,12 @@ namespace Neuron\Exceptions; -use Neuron\Core\Error; - /** * Class Deprecated * @package Neuron\Exceptions */ class Deprecated - extends Error + extends \Exception { } -?> diff --git a/src/Neuron/Exceptions/ExpectedType.php b/src/Neuron/Exceptions/ExpectedType.php new file mode 100755 index 0000000..54b8244 --- /dev/null +++ b/src/Neuron/Exceptions/ExpectedType.php @@ -0,0 +1,26 @@ + +} \ No newline at end of file diff --git a/src/Neuron/Exceptions/NotImplemented.php b/src/Neuron/Exceptions/NotImplemented.php index 37c426d..d8b48ba 100755 --- a/src/Neuron/Exceptions/NotImplemented.php +++ b/src/Neuron/Exceptions/NotImplemented.php @@ -3,12 +3,10 @@ namespace Neuron\Exceptions; -use Neuron\Core\Error; class NotImplemented - extends Error + extends \Exception { } -?> diff --git a/src/Neuron/Exceptions/OutputAlreadyStarted.php b/src/Neuron/Exceptions/OutputAlreadyStarted.php index b5cd9a1..8cbda89 100644 --- a/src/Neuron/Exceptions/OutputAlreadyStarted.php +++ b/src/Neuron/Exceptions/OutputAlreadyStarted.php @@ -7,7 +7,7 @@ class OutputAlreadyStarted - extends Error + extends \Exception { private $output; @@ -15,5 +15,4 @@ public function __construct ($output) { $this->output = $output; } -} -?> +} \ No newline at end of file diff --git a/src/Neuron/Exceptions/ResponseException.php b/src/Neuron/Exceptions/ResponseException.php new file mode 100644 index 0000000..bf915ea --- /dev/null +++ b/src/Neuron/Exceptions/ResponseException.php @@ -0,0 +1,35 @@ +response = $response; + parent::__construct('Response exception', $response->getStatus()); + } + + /** + * @return Response + */ + public function getResponse() + { + return $this->response; + } +} \ No newline at end of file diff --git a/src/Neuron/FrontController.php b/src/Neuron/FrontController.php deleted file mode 100644 index d2c1196..0000000 --- a/src/Neuron/FrontController.php +++ /dev/null @@ -1,157 +0,0 @@ -controllers[] = $controller; - $controller->setParentController ($this); - } - - public function canDispatch () - { - return true; - } - - public function setInput (array $fields) - { - $this->input = $fields; - } - - public function getInput ($id = null) - { - if ($id !== null) - { - if (isset ($this->input[$id])) - { - return $this->input[$id]; - } - return null; - } - return $this->input; - } - - public function setPage (Page $page) - { - $this->page = $page; - } - - public function getPage () - { - if (!isset ($this->page)) - { - $this->page = new Page (); - } - return $this->page; - } - - public function dispatch (Page $page = null) - { - Tracker::getInstance ()->setModule ('Neuron'); - - foreach ($this->controllers as $v) - { - if ($v->canDispatch ()) - { - return $v->dispatch ($this->getPage ()); - } - } - - // Nothing found? Last one it is. - if (count ($this->controllers) > 0) - { - return $this->controllers[count ($this->controllers) - 1]->dispatch ($this->getPage ()); - } - else - { - return Response::error ('No controllers set.'); - } - } - - public function setParentController (NeuronInterfacesFrontController $input) - { - // Nothing to do here. - } - - public function getName () - { - return 'Neuron front controller'; - } - - /** - * @param Request $request - */ - public function setRequest (Request $request) - { - $this->request = $request; - } - - /** - * @return Request - */ - public function getRequest () - { - return $this->request; - } - - /** - * @param Response $response - */ - public function setResponse (Response $response) - { - $this->response = $response; - } - - /** - * @return Response - */ - public function getResponse () - { - if (!isset ($this->response)) - { - $this->response = new Response (); - } - return $this->response; - } -} \ No newline at end of file diff --git a/src/Neuron/Interfaces/Controller.php b/src/Neuron/Interfaces/Controller.php new file mode 100644 index 0000000..a457f23 --- /dev/null +++ b/src/Neuron/Interfaces/Controller.php @@ -0,0 +1,28 @@ +mapped[$key])) + throw new InvalidParameter ("Mapper with name " . $key . " is already set."); + $this->mapped[$key] = $mapper; } - public function getMapper ($key, $default) + public function getMapper ($key) { - if (isset ($this->mapped[$key])) - { + if (isset ($this->mapped[$key])) { return $this->mapped[$key]; + } else { + throw new DataNotSet ("Mapper " . $key . " was not registered."); } - else - { - $this->mapped[$key] = new $default (); - } - return $this->mapped[$key]; } /** - * @return \Neuron\Mappers\UserMapper + * @return \Neuron\Interfaces\Mappers\UserMapper */ public static function getUserMapper () { - return self::getInstance ()->getMapper ('user', 'Neuron\Mappers\UserMapper'); + return self::getInstance ()->getMapper ('user'); } - /** - * @return \Neuron\Mappers\EmailMapper - */ - public static function getEmailMapper () - { - return self::getInstance ()->getMapper ('email', 'Neuron\Mappers\EmailMapper'); - } - - /** - * @return \Neuron\Mappers\AccountsMapper - */ - public static function getAccountMapper () - { - return self::getInstance ()->getMapper ('account', 'Neuron\Mappers\AccountsMapper'); - } } \ No newline at end of file diff --git a/src/Neuron/Mappers/BaseMapper.php b/src/Neuron/Mappers/BaseMapper.php new file mode 100644 index 0000000..18506ce --- /dev/null +++ b/src/Neuron/Mappers/BaseMapper.php @@ -0,0 +1,65 @@ + 0) + { + return $this->getObjectFromData ($data[0]); + } + return null; + } + + /** + * Override this to set an alternative object collection. + * @return Collection + */ + protected function getObjectCollection () + { + //return array (); + return new Collection (); + } + + /** + * @param $data + * @return array|mixed[] + */ + protected function getObjectsFromData ($data) + { + $out = $this->getObjectCollection (); + foreach ($data as $v) + { + $out[] = $this->getObjectFromData ($v); + } + return $out; + } + + protected abstract function getObjectFromData ($data); + +} \ No newline at end of file diff --git a/src/Neuron/Mappers/CachedMapper.php b/src/Neuron/Mappers/CachedMapper.php new file mode 100644 index 0000000..8c774d9 --- /dev/null +++ b/src/Neuron/Mappers/CachedMapper.php @@ -0,0 +1,89 @@ +models[$id]); + } + + /** + * Return model. + * @param $id + * @return Model|null + */ + protected function getModel ($id) { + return isset ($this->models[$id]) ? $this->models[$id] : null; + } + + /** + * @param $name + * @return bool + */ + protected function hasCollection ($name) { + return isset ($this->collections[$name]); + } + + /** + * @param $name + * @return null + */ + protected function getCollection ($name) { + return isset ($this->collections[$name]) ? $this->collections[$name] : null; + } + + protected function cache ($value, $name = null) { + if ($value instanceof Collection) { + + if (!isset ($name)) { + throw new InvalidParameter ("When caching a collection, you must provide a unique name."); + } + + // Set the collection. + $this->collections[$name] = $value; + + foreach ($value as $v) { + $this->cache ($v); + } + } + + else if ($value instanceof Model) { + $this->models[$value->getId ()] = $value; + } + + else if ($value === null) { + + if (!isset ($name)) { + throw new InvalidParameter ("When caching a null vlaue, you must provide a unique id."); + } + + $this->models[$name] = $value; + } + + else { + throw new InvalidParameter ("You must always cache collections or models."); + } + } + +} \ No newline at end of file diff --git a/src/Neuron/Mappers/EmailMapper.php b/src/Neuron/Mappers/EmailMapper.php deleted file mode 100644 index 3139278..0000000 --- a/src/Neuron/Mappers/EmailMapper.php +++ /dev/null @@ -1,150 +0,0 @@ -isConfirmed () ? 1 : 0; - - $id = $db->query - (" - INSERT INTO - users_email - SET - u_id = {$email->getUser ()->getId ()}, - ue_email = '{$db->escape ($email->getEmail ())}', - ue_code = '{$db->escape ($email->getCode ())}', - ue_confirmed = {$confirmed} - "); - - $email->setId ($id); - - return $email; - } - - public static function update (BMGroup_Models_Email $email) - { - $db = Neuron\DB\Database::getInstance (); - - $confirmed = $email->isConfirmed () ? 1 : 0; - - $db->query - (" - UPDATE - users_email - SET - ue_confirmed = {$confirmed} - WHERE - ue_id = {$email->getId ()} - "); - } - - public static function getEmails (BMGroup_Models_User $user) - { - $db = Neuron\DB\Database::getInstance (); - - $data = $db->query - (" - SELECT - * - FROM - users_email - WHERE - u_id = {$user->getId ()} - "); - - return self::getObjectsFromData ($data); - } - - public static function getFromCode ($id, $code) - { - $db = Neuron\DB\Database::getInstance (); - - $id = intval ($id); - - $data = $db->query - (" - SELECT - * - FROM - users_email - WHERE - ue_id = '{$id}' AND - ue_code = '{$db->escape ($code)}' - "); - - return self::getSingle ($data); - } - - public static function getPendingEmail (BMGroup_Models_User $user) - { - $db = Neuron\DB\Database::getInstance (); - - $data = $db->query - (" - SELECT - * - FROM - users_email - WHERE - u_id = {$user->getId ()} AND - ue_confirmed = 0 - "); - - return self::getObjectsFromData ($data); - } - - public static function removePendingEmails (BMGroup_Models_User $user) - { - $db = Neuron\DB\Database::getInstance (); - - $data = $db->query - (" - DELETE FROM - users_email - WHERE - u_id = {$user->getId ()} AND - ue_confirmed = 0 - "); - } - - private static function getSingle ($data) - { - if (count ($data) > 0) - { - return self::getObjectFromData ($data[0]); - } - return null; - } - - private static function getObjectsFromData ($data) - { - $out = array (); - foreach ($data as $v) - { - $out[] = self::getObjectFromData ($v); - } - return $out; - } - - private static function getObjectFromData ($v) - { - $tmp = new BMGroup_Models_Email ($v['ue_id']); - - $tmp->setUser (BMGroup_Mappers_UserMapper::getFromId ($v['u_id'])); - $tmp->setEmail ($v['ue_email']); - $tmp->setCode ($v['ue_code']); - $tmp->setConfirmed ($v['ue_confirmed'] == 1); - - return $tmp; - } - */ -} \ No newline at end of file diff --git a/src/Neuron/Mappers/UserMapper.php b/src/Neuron/Mappers/UserMapper.php deleted file mode 100644 index 8b25928..0000000 --- a/src/Neuron/Mappers/UserMapper.php +++ /dev/null @@ -1,413 +0,0 @@ -query - (" - SELECT - * - FROM - users - WHERE - u_id = '{$db->escape ($id)}' - "); - - $data = $this->getObjectsFromData ($data); - if (count ($data) > 0) - { - return $data[0]; - } - return null; - } - - /** - * @param int[] $ids - * @return User[] - */ - public function getFromIds ($ids) - { - if (count ($ids) == 0) - { - return array (); - } - - $list = array (); - foreach ($ids as $v) - { - $list[] = intval ($v); - } - $list = implode (',', $list); - - $db = Database::getInstance (); - - $data = $db->query - (" - SELECT - * - FROM - users - WHERE - u_id IN ({$list}) - "); - - return $this->getObjectsFromData ($data); - } - - /** - * @param $email - * @return User|null - */ - public function getFromEmail ($email) - { - $db = Database::getInstance (); - - $data = $db->query - (" - SELECT - * - FROM - users - WHERE - u_email = '{$db->escape ($email)}' - "); - - $data = $this->getObjectsFromData ($data); - if (count ($data) > 0) - { - return $data[0]; - } - return null; - } - - /** - * @param $email - * @param $password - * @return User|null - */ - public function getFromLogin ($email, $password) - { - $db = Database::getInstance (); - - $data = $db->query - (" - SELECT - * - FROM - users - WHERE - u_email = '{$db->escape ($email)}' AND - u_password = MD5(CONCAT('{$db->escape ($password)}', u_salt)) - "); - - $data = $this->getObjectsFromData ($data); - if (count ($data) > 0) - { - return $data[0]; - } - return null; - } - - /** - * Return hashed password & salt. - */ - private function hashPassword ($password) - { - // To be replaced by something more ... smart. - $salt = md5 (mt_rand (0, 1000000) . ' ' . time ()); - $password .= $salt; - return array (md5 ($password), $salt); - } - - /** - * @param User $user - * @return array - */ - protected function prepareFields (User $user) - { - // Hash the password & add some salt. - $out = array (); - - $password = $user->getPassword (); - if (!empty ($password)) - { - $hashes = $this->hashPassword ($user->getPassword ()); - - $out['u_password'] = $hashes[0]; - $out['u_salt'] = $hashes[1]; - } - - // Name & firstname - $name = $user->getName (); - $firstname = $user->getFirstname (); - if (!empty ($name)) - { - //$sql .= "u_name = '{$db->escape ($name)}', "; - $out['u_name'] = $name; - } - - if (!empty ($firstname)) - { - //$sql .= "u_firstname = '{$db->escape ($user->getFirstname ())}', "; - $out['u_firstname'] = $firstname; - } - - $email = $user->getEmail (); - if (!empty ($email)) - { - $out['u_email'] = $email; - } - else - { - //$sql .= "u_email = NULL, "; - $out['u_email'] = array (null, Query::PARAM_NULL, true); - } - - $mobile = $user->getMobile (); - if (!empty ($mobile)) - { - $out['u_mobile'] = $mobile; - } - else - { - $out['u_mobile'] = array (null, Query::PARAM_NULL, true); - } - - $out['u_isEmailValidated'] = 0; - $out['updated_at'] = array (time (), Query::PARAM_DATE); - - return $out; - } - - /** - * @param User $user - * @return User - */ - public function create (User $user) - { - $data = $this->prepareFields ($user); - - $data['created_at'] = array (time (), Query::PARAM_DATE); - - $query = Query::insert ('users', $data); - $id = $query->execute (); - - // Set ID in object - //$user->setId ($id); - - //return $user; - - return $this->getFromId ($id); - } - - /** - * @param User $user - */ - public function update (User $user) - { - $where = array ( - 'u_id' => $user->getId () - ); - - $query = Query::update ('users', $this->prepareFields ($user), $where); - $query->execute (); - } - - /** - * @param User $user - * @param $password - * @return bool - */ - public function checkPassword (User $user, $password) - { - $db = Database::getInstance (); - - $chk = $db->query - (" - SELECT - * - FROM - users - WHERE - u_id = {$user->getId ()} AND - u_password = MD5(CONCAT('{$db->escape ($password)}', u_salt)) - "); - - return count ($chk) > 0; - } - - /** - * @param User $user - * @return bool - */ - public function hasPassword (User $user) - { - $db = Database::getInstance (); - - $chk = $db->query - (" - SELECT - * - FROM - users - WHERE - u_id = {$user->getId ()} AND - u_password IS NOT NULL - "); - - return count ($chk) > 0; - } - - /** - * - */ - public function removeExpiredPasswordResetTokens () - { - $db = Database::getInstance (); - - $db->query - (" - DELETE FROM - users_passwordreset - WHERE - upw_date < NOW() - INTERVAL 1 DAY - "); - } - - /** - * @param User $user - * @param $token - * @param $ip - */ - public function addPasswordResetToken (User $user, $token, $ip) - { - $this->removeExpiredPasswordResetTokens (); - - $db = Database::getInstance (); - - $ip = inet_pton ($ip); - - $db->query - (" - INSERT INTO - users_passwordreset - SET - u_id = {$user->getId ()}, - upw_token = '{$db->escape ($token)}', - upw_date = NOW(), - upw_ip = '{$db->escape ($ip)}' - "); - } - - /** - * @param User $user - * @return array - */ - public function getPasswordResetTokens (User $user) - { - $db = Database::getInstance (); - - $data = $db->query - (" - SELECT - *, - UNIX_TIMESTAMP(upw_date) AS datum - FROM - users_passwordreset - WHERE - u_id = {$user->getId ()} AND - upw_date > NOW() - INTERVAL 1 DAY - "); - - $out = array (); - foreach ($data as $v) - { - $out[] = array - ( - 'code' => $v['upw_token'], - 'date' => $v['datum'], - 'ip' => inet_ntop ($v['upw_ip']) - ); - } - return $out; - } - - /** - * @param $data - * @return User[] - */ - public function getObjectsFromData ($data) - { - $out = array (); - foreach ($data as $v) - { - $out[] = $this->getObjectFromData ($v); - } - return $out; - } - - /** - * Return the name of the models that will be instanciated - * @return string - */ - public function getObjectClassname () - { - return 'Neuron\Models\User'; - } - - /** - * @param $data - * @return User|null - */ - public function getSingle ($data) - { - if (count ($data) > 0) - { - return $this->getObjectFromData ($data[0]); - } - return null; - } - - /** - * @param $data - * @return User - */ - public function getObjectFromData ($data) - { - $classname = $this->getObjectClassname (); - - /** @var \Neuron\Models\User $user */ - $user = new $classname ($data['u_id']); - - if (!empty ($data['u_email'])) - $user->setEmail ($data['u_email']); - - if (!empty ($data['u_name'])) - $user->setName ($data['u_name']); - - if (!empty ($data['u_firstname'])) - $user->setFirstname ($data['u_firstname']); - - if (!empty ($data['u_admin_status'])) - $user->setAdmin ($data['u_admin_status']); - - $user->setEmailValidated ($data['u_isEmailValidated'] == 1); - - return $user; - } -} \ No newline at end of file diff --git a/src/Neuron/Models/Error.php b/src/Neuron/Models/Error.php new file mode 100644 index 0000000..d31aa7f --- /dev/null +++ b/src/Neuron/Models/Error.php @@ -0,0 +1,110 @@ +template = $message; + $this->arguments = $arguments; + } + + /** + * @param string $subject + * @return $this + */ + public function setSubject($subject) + { + $this->subject = $subject; + return $this; + } + + /** + * @param $code + * @return $this + */ + public function setCode($code) + { + $this->code = $code; + return $this; + } + + /** + * @return string + */ + public function getSubject() + { + return $this->subject; + } + + /** + * @return string + */ + public function getTemplate() + { + return $this->template; + } + + /** + * @return mixed[] + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * @return string + */ + public function getCode() + { + return $this->code; + } + + /** + * @return string + */ + public function getMessage() + { + return vsprintf($this->getTemplate(), $this->getArguments()); + } + + /** + * @return string + */ + public function __toString() + { + return $this->getMessage(); + } +} diff --git a/src/Neuron/Models/Helpers/Errorable.php b/src/Neuron/Models/Helpers/Errorable.php index 94ac44b..20b2ee3 100644 --- a/src/Neuron/Models/Helpers/Errorable.php +++ b/src/Neuron/Models/Helpers/Errorable.php @@ -8,6 +8,9 @@ namespace Neuron\Models\Helpers; +use Neuron\Collections\ErrorCollection; +use Neuron\Models\Error; + /** * Class Errorable * @@ -18,45 +21,77 @@ abstract class Errorable { - /** - * @var string array - */ - private $errors = array (); - - /** - * @param string $error - */ - public function setError ($error) - { - $this->addError ($error); - } - - /** - * @return string|null - */ - public function getError () - { - if (count ($this->errors) > 0) - { - return end ($this->errors); - } - return null; - } - - /** - * @param $error - */ - public function addError ($error) - { - $this->errors[] = $error; - } - - /** - * @return string[] - */ - public function getErrors () - { - return $this->errors; - } - -} \ No newline at end of file + /** + * @var ErrorCollection array + */ + private $errors = null; + + /** + * + */ + private function touchErrors() + { + if (!isset ($this->errors)) { + $this->setErrors($this->createErrorCollection()); + } + } + + /** + * @return ErrorCollection + */ + protected function createErrorCollection() + { + return new ErrorCollection (); + } + + /** + * @param string $error + */ + public function setError($error) + { + return call_user_func_array(array($this, 'addError'), func_get_args()); + } + + /** + * Set the error array. By reference! + */ + public function setErrors(ErrorCollection $errors) + { + $this->errors = $errors; + } + + /** + * @return string|null + */ + public function getError() + { + $this->touchErrors(); + if (count($this->errors) > 0) { + return end($this->errors); + } + return null; + } + + /** + * @param $error + * @return Error + */ + public function addError($error) + { + $args = func_get_args(); + array_shift($args); + + $this->touchErrors(); + return $this->errors->addError($error, $args); + } + + /** + * @return ErrorCollection + */ + public function getErrors() + { + $this->touchErrors(); + return $this->errors; + } + +} diff --git a/src/Neuron/Models/Logger.php b/src/Neuron/Models/Logger.php index c492636..6292ce5 100644 --- a/src/Neuron/Models/Logger.php +++ b/src/Neuron/Models/Logger.php @@ -30,7 +30,12 @@ public static function getInstance () private $temporary = array (); private $temporarystart = array (); - private function __construct () + public static function start ($string, $replace = false, $color = null) + { + return self::getInstance ()->log ($string, $replace, $color); + } + + public function __construct () { $this->start = microtime(true); } @@ -168,4 +173,4 @@ public function flushTemporaryLog () return $out; } -} \ No newline at end of file +} diff --git a/src/Neuron/Models/Observable.php b/src/Neuron/Models/Observable.php new file mode 100644 index 0000000..6100582 --- /dev/null +++ b/src/Neuron/Models/Observable.php @@ -0,0 +1,76 @@ +events[$event])) + { + foreach ($this->events[$event] as $call) + { + call_user_func_array ($call, $arguments); + } + } + + // Notify everyone + foreach ($this->observing as $observer) + { + $observer->update (); + } + } + + /** + * Listen to specific events + * @param $event + * @param callable $callback + */ + public function on ($event, callable $callback) + { + if (!isset ($this->events[$event])) + { + $this->events[$event] = array (); + } + $this->events[$event][] = $callback; + } + + /** + * @param Observer $observer + */ + public function observe (Observer $observer) + { + $this->observing[] = $observer; + } +} \ No newline at end of file diff --git a/src/Neuron/Models/Router/Filter.php b/src/Neuron/Models/Router/Filter.php new file mode 100644 index 0000000..430430d --- /dev/null +++ b/src/Neuron/Models/Router/Filter.php @@ -0,0 +1,95 @@ +setName ($name); + } + + /** + * @return string + */ + public function getName () + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName ($name) + { + $this->name = $name; + } + + /** + * @return \mixed[] + */ + public function getArguments () + { + return $this->arguments; + } + + /** + * @param \mixed[] $arguments + */ + public function setArguments ($arguments) + { + $this->arguments = $arguments; + } + + /** + * @param Request $request + */ + public function setRequest (Request $request) + { + $this->request = $request; + } + + /** + * Clear the request + */ + public function clearRequest () + { + $this->request = null; + } + + /** + * @return Request + */ + public function getRequest () + { + return $this->request; + } + + /** + * @param $callback + * @param array $parameters + * @return mixed + */ + public function check ($callback, $parameters = []) + { + return call_user_func_array ($callback, array ($this, $parameters)); + } +} diff --git a/src/Neuron/Models/Router/Route.php b/src/Neuron/Models/Router/Route.php new file mode 100644 index 0000000..351c790 --- /dev/null +++ b/src/Neuron/Models/Router/Route.php @@ -0,0 +1,136 @@ +setRoute ($path); + } + + /** + * @return string + */ + public function getRoute () + { + return $this->route; + } + + /** + * @param string $route + */ + public function setRoute ($route) + { + $this->route = $route; + } + + /** + * @return callable + */ + public function getFunction () + { + return $this->function; + } + + /** + * @param callable $function + */ + public function setFunction ($function) + { + $this->function = $function; + } + + /** + * @return Module + */ + public function getModule () + { + return $this->module; + } + + /** + * @param Module $module + */ + public function setModule ($module) + { + $this->module = $module; + } + + /** + * Add a filter + * @param $filtername + * @return $this + */ + public function filter ($filtername) + { + $arguments = func_get_args (); + array_shift ($arguments); + + $filters = explode (',', $filtername); + foreach ($filters as $filter) + { + $objfilter = new Filter ($filter); + $objfilter->setArguments ($arguments); + + $this->filters[] = $objfilter; + } + + return $this; + } + + /** + * @return \mixed[] + */ + public function getParameters () + { + return $this->extraParameters; + } + + /** + * Add additional parameters that will be added to the controller call. + * @return $this + */ + public function with () { + + $this->extraParameters = func_get_args (); + return $this; + } + + /** + * @return Filter[] + */ + public function getFilters () + { + return $this->filters; + } + +} \ No newline at end of file diff --git a/src/Neuron/Models/User.php b/src/Neuron/Models/User.php deleted file mode 100644 index 5631bcc..0000000 --- a/src/Neuron/Models/User.php +++ /dev/null @@ -1,320 +0,0 @@ -setId ($id); - } - - public function setId ($id) - { - $this->id = $id; - } - - public function getId () - { - return intval ($this->id); - } - - public function setEmail ($email) - { - $this->email = $email; - } - - public function getEmail () - { - return $this->email; - } - - public function setMobile ($mobile) - { - $this->mobile = $mobile; - } - - public function getMobile () - { - return $this->mobile; - } - - /** - * get user is admin previleges level - * this is not related with admin of accounts - */ - public function getAdminRights () - { - return $this->admin; - } - - /** - * Only used during registration. - */ - public function setPassword ($password) - { - $this->password = $password; - } - - /** - * Only used during registration. - */ - public function getPassword () - { - return $this->password; - } - - public function setName ($name) - { - $this->name = $name; - } - - public function getName () - { - return $this->name; - } - - public function setFirstname ($name) - { - $this->firstname = $name; - } - - public function getFirstname () - { - return $this->firstname; - } - - public function getFullName () - { - return $this->getFirstname () . ' ' . $this->getName (); - } - - public function setAdmin ($admin_id) - { - $this->admin = $admin_id; - } - - public function getDisplayName () - { - return Tools::output_varchar ($this->getFullName ()); - } - - /** - * Return a list of all accounts this user has. - * Lazy loading, mysql query is done first time the accounts are requested. - * @return Account[] - */ - public function getAccounts () - { - if (!isset ($this->accounts)) - { - $this->accounts = MapperFactory::getAccountMapper ()->getFromUser ($this); - } - return $this->accounts; - } - - public function hasAccount (Account $type) - { - $typename = get_class ($type); - - foreach ($this->getAccounts () as $v) - { - if ($v instanceof $typename) - { - return true; - } - } - return false; - } - - /** - * Password. Now here is something special. - * It is possible that no password is set (when logged in through third party) - */ - public function hasPassword () - { - return MapperFactory::getUserMapper ()->hasPassword ($this); - } - - public function hasEmail () - { - $email = $this->getEmail (); - return !empty ($email); - } - - public function doesPasswordMatch ($password) - { - return MapperFactory::getUserMapper ()->checkPassword ($this, $password); - } - - public function changePassword ($oldpassword, $password, $repeatpassword, $ignoreOldPassword = false) - { - if (!$this->hasEmail ()) - { - $this->addError ('Please first set an email address before setting a password.'); - return false; - } - - if (empty ($password) || strlen ($password) < 6) - { - $this->addError ('Your password is too short. Please pick a password of at least 6 characters.'); - return false; - } - - if ($password != $repeatpassword) - { - $this->addError ('Your passwords do not match.'); - return false; - } - - if ($this->hasPassword () && !$ignoreOldPassword && !$this->doesPasswordMatch ($oldpassword)) - { - $this->addError ('Your old password is not correct.'); - return false; - } - - // Aaaand we change the password. - $this->addFeedback ('Your password was changed.'); - - $this->setPassword ($password); - MapperFactory::getUserMapper ()->update ($this); - - return true; - } - - public function getIp () - { - return isset ($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; - } - - /** - * Initialise a reset password procedure: - * send an email to the user. - */ - public function startResetPassword () - { - // Create the code - $code = TokenGenerator::getToken (10); - MapperFactory::getUserMapper ()->addPasswordResetToken ($this, $code, $this->getIp ()); - - $mail = Mailer::getInstance() - ->toUser($this) - ->setTemplate('reset_password/reset_password') - ->setAttribute('code_url', URLBuilder::getUrl ('login/lostpassword', array ('id' => $this->getId (), 'code' => $code))) - ->setAttribute('user-firstname', $this->getFirstname()) - ->setAttribute('logo', URLBuilder::getURL('assets/img/logo.png')) - ->schedule(); - } - - public function hasResetToken ($strcode) - { - $codes = MapperFactory::getUserMapper ()->getPasswordResetTokens ($this); - foreach ($codes as $code) - { - if ($code['code'] == $strcode) - { - return true; - } - } - - return false; - } - - private function addError ($message) - { - $this->errors[] = $message; - } - - public function getErrors () - { - return $this->errors; - } - - private function addFeedback ($message) - { - $this->feedbacks[] = $message; - } - - public function getFeedback () - { - return $this->feedbacks; - } - - public function setEmailValidated ($validated) - { - $this->emailValidated = $validated; - } - - /** - * Return true if email is validated - */ - public function isEmailValidated () - { - return $this->emailValidated; - } - - public function equals ($other) - { - return $this->getId () === $other->getId (); - } - - public function getData () - { - return array ( - 'id' => $this->getId (), - 'firstname' => $this->getFirstName (), - 'name' => $this->getName () - ); - } -} - -/** - * @SWG\Model( - * id="UserPasswordInput", - * @SWG\Property(name="oldpassword",type="string",required=true), - * @SWG\Property(name="newpassword",type="string",required=true) - * ) - */ \ No newline at end of file diff --git a/src/Neuron/Net/Client.php b/src/Neuron/Net/Client.php index b367c32..0dd9861 100644 --- a/src/Neuron/Net/Client.php +++ b/src/Neuron/Net/Client.php @@ -11,82 +11,165 @@ use Neuron\Exceptions\NotImplemented; -class Client { +class Client +{ - public static function getInstance () + public static function getInstance() { static $in; - if (!isset ($in)) - { + if (!isset ($in)) { $in = new self (); } return $in; } - private function __construct () + public static function http_parse_headers($raw_headers) { + $headers = array(); + $key = ''; // [+] + + foreach (explode("\n", $raw_headers) as $i => $h) { + $h = explode(':', $h, 2); + + if (isset($h[1])) { + if (!isset($headers[$h[0]])) + $headers[$h[0]] = trim($h[1]); + elseif (is_array($headers[$h[0]])) { + // $tmp = array_merge($headers[$h[0]], array(trim($h[1]))); // [-] + // $headers[$h[0]] = $tmp; // [-] + $headers[$h[0]] = array_merge($headers[$h[0]], array(trim($h[1]))); // [+] + } else { + // $tmp = array_merge(array($headers[$h[0]]), array(trim($h[1]))); // [-] + // $headers[$h[0]] = $tmp; // [-] + $headers[$h[0]] = array_merge(array($headers[$h[0]]), array(trim($h[1]))); // [+] + } + + $key = $h[0]; // [+] + } else // [+] + { // [+] + if (substr($h[0], 0, 1) == "\t") // [+] + $headers[$key] .= "\r\n\t" . trim($h[0]); // [+] + elseif (!$key) // [+] + $headers[0] = trim($h[0]); + trim($h[0]); // [+] + } // [+] + } + + return $headers; + } + + private function __construct() + { + + } + public function get(Request $request) + { + return $this->api($request, 'GET'); } - public function get (Request $request) + public function post(Request $request) { - return $this->api ($request, 'GET'); + return $this->api($request, 'POST'); } - public function post (Request $request) + public function put(Request $request) { - return $this->api ($request, 'POST'); + return $this->api($request, 'PUT'); } - public function put (Request $request) + public function delete(Request $request) { - return $this->api ($request, 'PUT'); + return $this->api($request, 'DELETE'); } - public function delete (Request $request) + public function process(Request $request) { - return $this->api ($request, 'DELETE'); + return $this->api($request, $request->getMethod()); } - private function api (Request $request, $method) + private function api(Request $request, $method) { $ch = curl_init(); - $post = $request->getBody (); + $post = $request->getBody(); - curl_setopt($ch, CURLOPT_URL, $request->getUrl () . '?' . $request->getParameters ()); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $parsedUrl = $request->getUrl(); + + if ($request->getParameters()) { + + if (strpos($parsedUrl, '?')) { + $parsedUrl .= '&'; + } else { + $parsedUrl .= '?'; + } - $headers = array (); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + $parsedUrl .= http_build_query($request->getParameters()); + }; + + curl_setopt($ch, CURLOPT_URL, $parsedUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, 1); + + if ($request->getHeaders()) { + $headers = []; + foreach ($request->getHeaders() as $k => $v) { + switch ($k) { + case 'User-Agent': + curl_setopt($ch, CURLOPT_USERAGENT, $v); + break; + + default: + $headers[] = $k . ': ' . $v; + break; + } + } + + if (count($headers) > 0) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + } + } - switch ($method) - { + switch ($method) { case 'GET': - break; + break; case 'POST': curl_setopt($ch, CURLOPT_POST, 1); - break; + + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post); + break; case 'DELETE': throw new NotImplemented ("Not implemented."); - break; + break; case 'PUT': curl_setopt($ch, CURLOPT_PUT, 1); - break; - } - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post); + break; + + } $output = curl_exec($ch); // Response - $response = new Response (); - $response->setBody ($output); + + + $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $header = substr($output, 0, $header_size); + $body = substr($output, $header_size); + + $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + $response = Response::fromRaw($body, self::http_parse_headers($header)); + curl_close($ch); + + $response->setStatus($status); + return $response; } - -} \ No newline at end of file +} diff --git a/src/Neuron/Net/Entity.php b/src/Neuron/Net/Entity.php index bc0ec5b..a48cc0b 100644 --- a/src/Neuron/Net/Entity.php +++ b/src/Neuron/Net/Entity.php @@ -1,131 +1,141 @@ getFromId ($data['session']['user']); - if ($user) - { - $model->setUser ($user); - } - } - // The body. If data is found, body is not used. - if (isset ($data['data']) && !empty ($data['data'])) - { - $model->setData ($data['data']); + if (isset ($data['data']) && !empty ($data['data'])) { + $model->setData($data['data']); + } else if (isset ($data['body'])) { + $model->setBody($data['body']); } - else if (isset ($data['body'])) - { - $model->setBody ($data['body']); + if (isset ($data['headers'])) { + $model->setHeaders($data['headers']); } - if (isset ($data['session'])) - { - $model->setSession ($data['session']); + if (isset ($data['cookies'])) { + $model->setCookies($data['cookies']); } - if (isset ($data['headers'])) - { - $model->setHeaders ($data['headers']); + if (isset ($data['post'])) { + $model->setPost($data['post']); } - if (isset ($data['cookies'])) - { - $model->setCookies ($data['cookies']); - } - - if (isset ($data['post'])) - { - $model->setPost ($data['post']); - } - - if (isset ($data['status'])) - { - $model->setStatus ($data['status']); + if (isset ($data['status'])) { + $model->setStatus($data['status']); } return $model; } + /** + * Parse data based on content type. + */ + protected function parseData() + { + $contentType = $this->getHeader('content-type'); + if ($contentType) { + switch (strtolower($contentType)) { + case 'application/json': + case 'text/json': + + $data = json_decode($this->getBody(), true); + + if (!$data) { + $this->setError('JSON decode error: ' . json_last_error_msg()); + } else { + $this->setData($data); + } + + break; + + default: + $this->setData($_POST); + } + } + } + /** * @return array */ - public function getJSONData () + public function getJSONData() { - $data = array (); + $data = array(); - if ($this->getData ()) - { - $plaindata = $this->getData (); - if (!empty ($plaindata)) - { - $data['data'] = $this->getData (); + if ($this->getData()) { + $plaindata = $this->getData(); + if (!empty ($plaindata)) { + $data['data'] = $this->getData(); } } - - $data['body'] = $this->getBody (); - $data['session'] = $this->getSession (); - $data['headers'] = $this->getHeaders (); - $data['cookies'] = $this->getCookies (); - $data['post'] = $this->getPost (); - $data['status'] = $this->getStatus (); + $data['body'] = $this->getBody(); + + $data['headers'] = $this->getHeaders(); + $data['cookies'] = $this->getCookies(); + $data['post'] = $this->getPost(); + $data['status'] = $this->getStatus(); return $data; } @@ -134,23 +144,22 @@ public function getJSONData () * @param array $data * @return string */ - private static function calculateSignature (array $data) + private static function calculateSignature(array $data) { - $txt = self::calculateBaseString ($data); - return md5 ($txt); + $txt = self::calculateBaseString($data); + return md5($txt); } - private static function calculateBaseString (array $data) + private static function calculateBaseString(array $data) { unset ($data['signature']); $txt = '\(^-^)/ !Stupid Rainbow Tables! \(^-^)/ '; - foreach ($data as $k => $v) - { - $txt .= $k . ":" . json_encode ($v) . "|"; + foreach ($data as $k => $v) { + $txt .= $k . ":" . json_encode($v) . "|"; } - $txt .= APP_SECRET_KEY; + $txt .= Config::get('app.secret'); return $txt; } @@ -158,196 +167,266 @@ private static function calculateBaseString (array $data) /** * @return string json */ - public function toJSON () + public function toJSON() { - $data = $this->getJSONData (); - $data['random'] = mt_rand (); - $data['time'] = gmdate ('c'); - $data['signature'] = $this->calculateSignature ($data); - return json_encode ($data); + $data = $this->getJSONData(); + $data['random'] = mt_rand(); + $data['time'] = gmdate('c'); + $data['signature'] = $this->calculateSignature($data); + return json_encode($data); } /** - * @param array|string $key - * @param mixed $value + * @return \Neuron\Net\Session + * @throws DataNotSet */ - public function setSession ($key, $value = null) + public function getSession() { - if (is_array ($key) && is_null ($value)) - { - $this->session = $key; - } - else - { - if (!isset ($this->session)) - { - $this->session = array (); + if (!isset ($this->session)) { + // First check the router + if ($this instanceof Response) { + $router = Application::getInstance()->getRouter(); + if ($router) { + $this->session = $router->getRequest()->getSession(); + } + } else { + throw new DataNotSet ("No session is set in the request."); } - $this->session[$key] = $value; } - } - - /** - * @return mixed - */ - public function getSession () - { return $this->session; } /** - * @param User $user + * @param Session $session + * @return $this */ - public function setUser (User $user) + public function setSession(Session $session) { - $this->user = $user; - $this->setSession ('user', $user->getId ()); - } - - /** - * @return User - */ - public function getUser () - { - return $this->user; + $this->session = $session; + return $this; } /** * @param $body + * @return $this */ - public function setBody ($body) + public function setBody($body) { $this->body = $body; + return $this; } /** * @return mixed */ - public function getBody () + public function getBody() { return $this->body; } /** * @param $id + * @return $this */ - public function setApplication ($id) + public function setApplication($id) { - $this->setSession ('application', $id); + $this->setSession('application', $id); + return $this; } /** * @return mixed */ - public function getApplication () + public function getApplication() { return $this->application; } /** * @param $path + * @return $this */ - public function setPath ($path) + public function setPath($path) { $this->path = $path; + return $this; } /** * @return mixed */ - public function getPath () + public function getPath() { return $this->path; } /** * @param array $post + * @return $this */ - public function setPost ($post) + public function setPost($post) { $this->post = $post; + return $this; } /** * @return array */ - public function getPost () + public function getPost() { return $this->post; } /** * @param mixed $data + * @return $this */ - public function setData ($data) + public function setData($data) { $this->data = $data; + return $this; } /** * @return mixed */ - public function getData () + public function getData() { return $this->data; } + /** + * Check if request has data + */ + public function hasData() + { + if (!isset ($this->data)) { + if (!isset ($this->error)) + $this->setError('No input data set'); + + return false; + } else { + return true; + } + } + /** * @param string $name * @param string $value + * @return $this */ - public function setHeader ($name, $value = null) + public function setHeader($name, $value = null) { - if (!isset ($this->headers)) - { - $this->headers = array (); + if (!isset ($this->headers)) { + $this->headers = array(); } $this->headers[$name] = $value; + return $this; } /** * @param array $headers + * @return $this */ - public function setHeaders ($headers) + public function setHeaders($headers) { $this->headers = $headers; + return $this; } /** * @return array */ - public function getHeaders () + public function getHeaders() { return $this->headers; } - public function setCookies ($cookies) + /** + * @param $name + * @return string|null + */ + public function getHeader($name) + { + $name = $this->getSafeHeaderName($name); + foreach ($this->getHeaders() as $k => $v) { + if ($this->getSafeHeaderName($k) === $name) { + return $v; + } + } + return null; + } + + /** + * @param $name + * @return string + */ + protected function getSafeHeaderName($name) + { + $name = str_replace('-', '_', $name); + $name = str_replace('_', ' ', $name); + $name = strtolower($name); + $name = ucwords($name); + $name = str_replace(' ', '-', $name); + + return $name; + } + + public function setCookies($cookies) { $this->cookies = $cookies; + return $this; } - public function getCookies () + public function getCookies() { return $this->cookies; } - public function setStatus ($status) + public function setStatus($status) { $this->status = $status; + return $this; } - public function isStatusSet () + public function isStatusSet() { return isset ($this->status); } - public function getStatus () + public function getStatus() { - if (isset ($this->status)) - { + if (isset ($this->status)) { return $this->status; } return 200; } -} \ No newline at end of file + /** + * @param $error + * @return $this + */ + public function setError($error) + { + $this->error = $error; + return $this; + } + + /** + * @return string + */ + public function getError() + { + return $this->error; + } + + /** + * Return an error response. + * @return Response + */ + public function getErrorResponse() + { + return Response::error($this->getError(), self::STATUS_INVALID_INPUT); + } +} diff --git a/src/Neuron/InputStream.php b/src/Neuron/Net/InputStream.php similarity index 97% rename from src/Neuron/InputStream.php rename to src/Neuron/Net/InputStream.php index 50c400e..b9bbe09 100644 --- a/src/Neuron/InputStream.php +++ b/src/Neuron/Net/InputStream.php @@ -6,7 +6,7 @@ * Time: 13:57 */ -namespace Neuron; +namespace Neuron\Net; use Neuron\Core\Tools; diff --git a/src/Neuron/Net/Outputs/HTML.php b/src/Neuron/Net/Outputs/HTML.php index 6c397da..bb3118d 100644 --- a/src/Neuron/Net/Outputs/HTML.php +++ b/src/Neuron/Net/Outputs/HTML.php @@ -29,7 +29,12 @@ public function output (Response $response) { foreach ($response->getHeaders () as $k => $v) { - header ($k . ': ' . $v); + if (!empty ($v)) { + header ($k . ': ' . $v); + } + else { + header ($k); + } } } diff --git a/src/Neuron/Net/Outputs/Raw.php b/src/Neuron/Net/Outputs/Raw.php new file mode 100644 index 0000000..a063f1b --- /dev/null +++ b/src/Neuron/Net/Outputs/Raw.php @@ -0,0 +1,28 @@ +getBody ()) + { + echo $response->getBody (); + } + else + { + echo $response->getData (); + } + } + +} \ No newline at end of file diff --git a/src/Neuron/Net/Outputs/XML.php b/src/Neuron/Net/Outputs/XML.php new file mode 100644 index 0000000..1f0cb40 --- /dev/null +++ b/src/Neuron/Net/Outputs/XML.php @@ -0,0 +1,172 @@ +root = $root; + $this->version = $version; + $this->itemName = $itemName; + $this->parameters = $parameters; + } + + public static function writexml (XMLWriter $xml, $data, $item_name = 'item') + { + foreach($data as $key => $value) + { + if (is_int ($key)) + { + $key = $item_name; + } + + if (is_array($value)) + { + if ($key != 'items') + { + $xml->startElement($key); + } + + if (isset ($value['attributes']) && is_array ($value['attributes'])) + { + foreach ($value['attributes'] as $k => $v) + { + $xml->writeAttribute ($k, $v); + } + + unset ($value['attributes']); + } + + self::writexml ($xml, $value, substr ($key, 0, -1)); + + if ($key != 'items') + { + $xml->endElement(); + } + } + + elseif ($key == 'element-content') + { + $xml->text ($value); + } + + else + { + $xml->writeElement($key, $value); + } + } + } + + public static function output_xml ($data, $version = '0.1', $root = 'root', $parameters = array (), $sItemName = 'item') + { + $xml = new XmlWriter(); + $xml->openMemory(); + $xml->startDocument('1.0', 'UTF-8'); + $xml->startElement($root); + $xml->setIndent (true); + + if (!empty ($version)) + { + $xml->writeAttribute ('version', $version); + } + + foreach ($parameters as $paramk => $paramv) + { + $xml->writeAttribute ($paramk, $paramv); + } + + self::writexml ($xml, $data, $sItemName); + + $xml->endElement(); + return $xml->outputMemory(true); + } + + private static function xml_escape ($input) + { + //$input = str_replace ('"', '"', $input); + //$input = str_replace ("'", ''', $input); + + + $input = str_replace ('<', '<', $input); + $input = str_replace ('>', '>', $input); + $input = str_replace ('&', '&', $input); + + + return $input; + } + + public static function output_partly_xml ($data, $key = null) + { + $output = '<'.$key; + + if (isset ($data['attributes']) && is_array ($data['attributes'])) + { + foreach ($data['attributes'] as $k => $v) + { + $output .= ' '.$k.'="'.$v.'"'; + } + + unset ($data['attributes']); + } + + $output .= '>'; + if (!is_array ($data)) + { + $output .= self::xml_escape ($data); + } + + elseif (count ($data) == 1 && isset ($data['element-content'])) + { + $output .= self::xml_escape ($data['element-content']); + } + + else + { + foreach ($data as $k => $v) + { + if (is_numeric ($k)) + { + $k = substr ($key, 0, -1); + } + + $output .= self::output_partly_xml ($v, $k); + } + } + $output .= ''."\n"; + + return $output; + } + + public function outputContent (Response $response) + { + header ('Content-type: application/xml'); + + if (!is_string ($response->getData ())) + { + echo self::output_xml ($response->getData (), $this->version, $this->root, $this->parameters, $this->itemName); + } + + else + { + echo $response->getData (); + } + } + + +} \ No newline at end of file diff --git a/src/Neuron/Net/QueryTrackingParameters.php b/src/Neuron/Net/QueryTrackingParameters.php new file mode 100644 index 0000000..5a44141 --- /dev/null +++ b/src/Neuron/Net/QueryTrackingParameters.php @@ -0,0 +1,44 @@ +queryParameters = Config::get('tracking.queryParameters'); + } + } + return $in; + } + + public $queryParameters = [ + 'utm_abversion', + 'utm_referrer', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + '_ga', + '_gac', + 'pk_vid', + '_gl', + '_cc' + ]; +} diff --git a/src/Neuron/Net/Request.php b/src/Neuron/Net/Request.php index 44fac75..035531d 100644 --- a/src/Neuron/Net/Request.php +++ b/src/Neuron/Net/Request.php @@ -9,14 +9,30 @@ namespace Neuron\Net; +use DateTime; +use Exception; +use Neuron\Core\Tools; use Neuron\Exceptions\InvalidParameter; -use Neuron\InputStream; -use Neuron\MapperFactory; -use Neuron\Models\User; +use Neuron\Interfaces\Models\User; +use Neuron\Models\Router\Route; class Request extends Entity { + const METHOD_POST = 'POST'; + const METHOD_GET = 'GET'; + const METHOD_PATCH = 'PATCH'; + const METHOD_PUT = 'PUT'; + const METHOD_OPTIONS = 'OPTIONS'; + + /** @var User $user */ + private $user; + + /** @var callable[] $usercallback */ + private $usercallback = array (); + + private $usercallbackcalled = false; + /** * @return Request */ @@ -25,8 +41,8 @@ public static function fromInput () global $module; $model = new self (); + $model->setMethod (self::getMethodFromInput ()); - $model->setMethod (isset($_SERVER['REQUEST_METHOD'])? $_SERVER['REQUEST_METHOD']: null); if (isset ($module)) { $model->setPath ($module); @@ -36,16 +52,90 @@ public static function fromInput () } $model->setBody (InputStream::getInput ()); - $model->setHeaders (getallheaders ()); + $model->setHeaders (self::getHeadersFromInput ()); $model->setParameters ($_GET); $model->setCookies ($_COOKIE); $model->setPost ($_POST); $model->setEnvironment ($_SERVER); $model->setStatus (http_response_code ()); + $model->setUrl (self::getCurrentUri ()); + + $model->parseData (); return $model; } + /** + * Get all request headers + * @return array The request headers + */ + private static function getHeadersFromInput () + { + // getallheaders available, use that + if (function_exists('getallheaders')) return getallheaders(); + + // getallheaders not available: manually extract 'm + $headers = []; + foreach ($_SERVER as $name => $value) + { + if (substr($name, 0, 5) == 'HTTP_') { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); + $headers[$key] = $value; + } else if (strtolower (substr ($name, 0, 7)) == 'content') { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $name)))); + $headers[$key] = $value; + } + } + return $headers; + } + + /** + * Get the request method used, taking overrides into account + * @return string The Request method to handle + */ + private static function getMethodFromInput () + { + // Take the method as found in $_SERVER + $method = $_SERVER['REQUEST_METHOD']; + + // If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification + // @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4 + if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { + ob_start(); + $method = 'GET'; + } + + // If it's a POST request, check for a method override header + else if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $headers = self::getHeadersFromInput (); + if (isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], array('PUT', 'DELETE', 'PATCH'))) { + $method = $headers['X-HTTP-Method-Override']; + } + } + + return $method; + } + + /** + * Define the current relative URI + * @return string + */ + private static function getCurrentUri () + { + // Get the current Request URI and remove rewrite basepath from it (= allows one to run the router in a subfolder) + $basepath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/'; + $uri = substr($_SERVER['REQUEST_URI'], strlen($basepath)); + + // Don't take query params into account on the URL + if (strstr($uri, '?')) $uri = substr($uri, 0, strpos($uri, '?')); + + // Remove trailing slash + enforce a slash at the start + $uri = '/' . trim($uri, '/'); + + return $uri; + + } + /** * @param $json * @return Request @@ -71,7 +161,7 @@ public static function fromJSON ($json) { $model->setSegments ($data['segments']); } - + if (isset ($data['environment'])) { $model->setEnvironment ($data['environment']); @@ -80,6 +170,41 @@ public static function fromJSON ($json) return $model; } + /** + * Return a request based on a simple url. + * @param string $url + * @param string $stripHost + * @return Request + */ + public static function fromURL ($url, $stripHost) { + + $data = parse_url ($url); + + $parameters = array (); + + if (!empty ($data['query'])) + parse_str ($data['query'], $parameters); + + $request = new self (); + + if ($stripHost) { + $request->setUrl ($data['path']); + } + + else { + $request->setUrl ( + $data['scheme'] . '://' . + $data['host'] . (isset ($data['port']) ? ':' . $data['port'] : '') . + $data['path'] + ); + } + + $request->setParameters ($parameters); + + return $request; + + } + private $method = 'GET'; private $url; private $parameters; @@ -88,10 +213,12 @@ public static function fromJSON ($json) /** * @param $method + * @return $this */ public function setMethod ($method) { $this->method = $method; + return $this; } /** @@ -104,10 +231,12 @@ public function getMethod () /** * @param $url + * @return $this */ public function setUrl ($url) { $this->url = $url; + return $this; } /** @@ -120,18 +249,22 @@ public function getUrl () /** * @param array $parameters + * @return $this */ public function setParameters (array $parameters) { $this->parameters = $parameters; + return $this; } /** * @param $string + * @return $this */ public function setQueryString ($string) { $this->parameters = parse_url ($string); + return $this; } /** @@ -144,10 +277,12 @@ public function getParameters () /** * @param $input + * @return $this */ public function setSegments ($input) { $this->input = $input; + return $this; } /** @@ -194,10 +329,226 @@ public function getJSONData () public function setEnvironment ($data) { $this->environment = $data; + return $this; } public function getEnvironment () { return $this->environment; } -} \ No newline at end of file + + /** + * @return bool + */ + public function isPost () + { + return $this->getMethod () === self::METHOD_POST; + } + + /** + * @return bool + */ + public function isGet () + { + return $this->getMethod () === self::METHOD_GET; + } + + /** + * @return bool + */ + public function isPut () + { + return $this->getMethod () === self::METHOD_PUT; + } + + /** + * @return bool + */ + public function isPatch () + { + return $this->getMethod () === self::METHOD_PATCH; + } + + /** + * @return bool + */ + public function isOptions () + { + return $this->getMethod () === self::METHOD_OPTIONS; + } + + /** + * Similar to fetching a value from $_REQUEST + * @param $field + * @param string $type + * @param mixed $default + * @param string $separator + * @return mixed|null + */ + public function input ($field, $type = 'string', $default = null, $separator = ',') + { + // Check for array type + $array = false; + if (substr ($type, -2) === '[]') { + $arrtype = substr ($type, 0, -2); + $type = 'string'; + $array = true; + } + + // Check post + $value = Tools::getInput ($this->getPost (), $field, $type); + if ($value === null) + { + // Check get + $value = Tools::getInput ($this->getParameters (), $field, $type); + } + + if ($value === null) + { + return $default; + } + + // Check if array? + if ($array) { + $values = explode ($separator, $value); + + $value = array (); + foreach ($values as $v) { + if (Tools::checkInput ($v, $arrtype)) { + $value[] = $v; + } + } + } + + return $value; + } + + /** + * Helper method to make it easier for authentication modules + * @param User $user + * @return $this + */ + public function setUser (User $user = null) + { + $this->user = $user; + return $this; + } + + /** + * To allow lazy loading of the user object, set a callback here. + * Method will be called with request as parameter and only once a script. + * @param callable $callback + * @throws InvalidParameter + * @return $this + */ + public function setUserCallback (callable $callback) + { + //$this->usercallback = $callback; + + if (count ($this->usercallback) > 0) + throw new InvalidParameter ("A usercallback was already set. Use addUserCallback to add multiple callbacks"); + + $this->addUserCallback ('default', $callback); + + return $this; + } + + /** + * To allow for multiple authentication methods, extra user callbacks can be set. + * Each callback must have a unique name. This name can be used in getUser to force + * @param $name + * @param callable $callback + * @throws InvalidParameter + * @return $this + */ + public function addUserCallback ($name, callable $callback) + { + if (isset ($this->usercallback[$name])) + throw new InvalidParameter ("A usercallback with name " . $name . " is already set. Each callback must have a unique name."); + + $this->usercallback[$name] = $callback; + + return $this; + } + + /** + * @param string $callbackName To force a specific callback + * @return \Neuron\Interfaces\Models\User + */ + public function getUser ($callbackName = null) + { + if (!isset ($this->user) && !$this->usercallbackcalled) + { + $this->usercallbackcalled = true; + + if (isset ($this->usercallback[$callbackName])) + { + $this->user = call_user_func ($this->usercallback[$callbackName], $this); + } + + else { + // Loop trough all callbacks until we find one that returns something + $user = null; + foreach ($this->usercallback as $cb) + { + $user = call_user_func ($cb, $this); + if ($user) + break; + } + $this->user = $user; + } + } + + return $this->user; + } + + /** + * + */ + public function clearUser() + { + $this->setUser(null); + $this->usercallbackcalled = false; + } + + /** + * Evaluate a route and return the parameters. + * @param Route $route + * @return array|bool + */ + public function parseRoute (Route $route) { + + if (preg_match_all('#^' . $route->getRoute () . '$#', $this->getUrl (), $matches, PREG_OFFSET_CAPTURE)) { + + // Rework matches to only contain the matches, not the orig string + $matches = array_slice($matches, 1); + + // Extract the matched URL parameters (and only the parameters) + $params = array_map(function($match, $index) use ($matches) { + + // We have a following parameter: take the substring from the current param position until the next one's position (thank you PREG_OFFSET_CAPTURE) + if ( + isset($matches[$index+1]) && + isset($matches[$index+1][0]) && + is_array($matches[$index+1][0]) && + $matches[$index+1][0][1] >= 0 + ) { + return trim(substr($match[0][0], 0, $matches[$index+1][0][1] - $match[0][1]), '/'); + } else { + // We have no following paramete: return the whole lot + return (isset($match[0][0]) ? trim($match[0][0], '/') : null); + } + + }, $matches, array_keys($matches)); + + if (empty ($params)) { + return true; + } + else { + return $params; + } + } + + return false; + } +} diff --git a/src/Neuron/Net/Response.php b/src/Neuron/Net/Response.php index 43fb6cc..18f9709 100644 --- a/src/Neuron/Net/Response.php +++ b/src/Neuron/Net/Response.php @@ -9,12 +9,14 @@ namespace Neuron\Net; +use Neuron\Core\Template; use Neuron\Models\User; use Neuron\Net\Outputs\HTML; use Neuron\Net\Outputs\JSON; use Neuron\Net\Outputs\Output; use Neuron\Net\Outputs\PrintR; use Neuron\Net\Outputs\Table; +use Neuron\Net\Outputs\XML; class Response extends Entity { @@ -55,6 +57,17 @@ public static function fromJSON ($json) return $model; } + public static function fromRaw ($body, $headers) { + + $out = new self (); + $out->setBody ($body); + $out->setHeaders ($headers); + $out->parseData (); + + return $out; + + } + /** * Show the data in a html table. * @param $data @@ -89,21 +102,110 @@ public static function json ($data) return $in; } - public static function error ($data) + /** + * @param $data + * @param string $root XML Root element name + * @param string $version Root element version number + * @param array $parameters Array of root element attributes + * @param string $itemName Generic item name. + * @return Response + */ + public static function xml ($data, $root = 'root', $version = '1.0', array $parameters = array (), $itemName = 'item') { $in = new self (); $in->setData ($data); - $in->setData (new HTML ()); + $in->setOutput (new XML ($root, $version, $parameters, $itemName)); return $in; } + /** + * @param string $message + * @param int $statuscode + * @return Response + */ + public static function error ($message, $statuscode = 500) + { + $template = new Template ('error.phpt'); + $template->set ('message', $message); + $template->set ('status', $statuscode); + + $response = self::template ($template); + $response->setStatus ($statuscode); + + + return $response; + } + + /** + * @param Template|string $template + * @param array $data + * @return Response + */ + public static function template ($template, $data = array ()) + { + $in = new self (); + + if (! ($template instanceof Template)) + { + $template = new Template ($template); + + } + + foreach ($data as $k => $v) + { + $template->set ($k, $v); + } + + $in->setTemplate ($template); + + return $in; + } + + /** + * Proxy a request. + * @param string|Request $request Url or Request object. + * @return Response + */ + public static function proxy ($request) { + + if (!$request instanceof Request) { + $url = $request; + + $request = new Request (); + $request->setUrl ($url); + $request->setMethod (Request::METHOD_GET); + } + + return Client::getInstance ()->process ($request); + } + private $output; - public function redirect ($url) + /** + * Create a redirect response. + * @param $url + * @return Response + */ + public static function redirect ($url) + { + $response = new self (); + $response->setRedirect ($url); + + return $response; + } + + /** + * Set a response to be a redirect. + * @param $url + * @return $this + */ + public function setRedirect ($url) { $this->setHeader ('Location', $url); $this->setStatus (302); $this->setData (array ('message' => 'Redirecting to ' . $url)); + + return $this; } public function getJSONData () @@ -118,9 +220,18 @@ public function getJSONData () return $data; } + private function setTemplate (Template $template) + { + $this->setBody ($template->parse ()); + $this->setOutput (new HTML ()); + + return $this; + } + public function setOutput (Output $output) { $this->output = $output; + return $this; } public function isOutputSet () @@ -147,4 +258,39 @@ public function output () { $this->getOutput ()->output ($this); } + + public function setETag ($tag) { + $this->setHeader ('etag', $tag); + } + + public function setNoCache () { + $this->setHeader ('Cache-Control', 'private, max-age=0, no-cache'); + $this->setHeader ('Pragma', 'no-cache'); + + $date = time (); + $date -= (60 * 60 * 24 * 7); + + $this->setHeader ('Expires', date ('c', $date)); + } + + public function setCache ($maxAge = 86400, $privacy = 'public') { + + switch ($privacy) { + case 'public': + case 'private': + break; + + default: + $privacy = 'private'; + break; + } + + $this->setHeader ('Cache-Control', $privacy); + + $date = time (); + $date += $maxAge; + + $this->setHeader ('Expires', date ('c', $date)); + + } } \ No newline at end of file diff --git a/src/Neuron/Net/Session.php b/src/Neuron/Net/Session.php new file mode 100644 index 0000000..3d7d843 --- /dev/null +++ b/src/Neuron/Net/Session.php @@ -0,0 +1,91 @@ +handler = $handler; + } + + public function getSessionId () + { + return session_id (); + } + + /** + * Start a session + * @param string $sessionId + */ + public function connect ($sessionId = null) + { + $this->handler->start ($sessionId); + } + + /** + * Disconnect session handler + */ + public function disconnect () + { + if ($this->handler) + { + $this->handler->stop (); + } + } + + /** + * Destroy a session + */ + public function destroy () + { + // Unset all of the session variables. + $_SESSION = array(); + + // If it's desired to kill the session, also delete the session cookie. + // Note: This will destroy the session, and not just the session data! + if (ini_get("session.use_cookies")) { + $params = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, + $params["path"], $params["domain"], + $params["secure"], $params["httponly"] + ); + } + + // Finally, destroy the session. + session_destroy(); + } + + public function set ($key, $value) + { + $_SESSION[$key] = $value; + } + + public function all () + { + return $_SESSION; + } + + public function get ($key) + { + if (isset ($_SESSION[$key])) + { + return $_SESSION[$key]; + } + return null; + } + + /** + * @return string + */ + public function getSessionQueryString() + { + return $this->handler->getSessionQueryString(); + } +} diff --git a/src/Neuron/Page.php b/src/Neuron/Page.php deleted file mode 100644 index 92e4b1d..0000000 --- a/src/Neuron/Page.php +++ /dev/null @@ -1,38 +0,0 @@ -content = $content; - } - - public function setTitle ($title) - { - $this->title = $title; - } - - public function getOutput () - { - $html = new Template (); - - $html->set ('title', $this->title); - $html->set ('content', $this->content); - - return $html->parse ('index.phpt'); - } -} \ No newline at end of file diff --git a/src/Neuron/Router.php b/src/Neuron/Router.php index 1508f3a..ec5f18e 100644 --- a/src/Neuron/Router.php +++ b/src/Neuron/Router.php @@ -1,29 +1,5 @@ * @copyright Copyright (c), 2013 Bram(us) Van Damme @@ -32,6 +8,15 @@ namespace Neuron; +use Neuron\Exceptions\InvalidParameter; +use Neuron\Exceptions\ResponseException; +use Neuron\Interfaces\Controller; +use Neuron\Interfaces\Module; +use Neuron\Models\Router\Route; +use Neuron\Net\Request; +use Neuron\Net\Response; +use Neuron\Tools\ControllerFactory; + class Router { @@ -40,13 +25,6 @@ class Router { */ private $routes = array(); - - /** - * @var array The before middleware route patterns and their handling functions - */ - private $befores = array(); - - /** * @var object The function to be executed when no route has been matched */ @@ -64,26 +42,28 @@ class Router { */ private $method = ''; - /** - * Store a before middleware route and a handling function to be executed when accessed using one of the specified methods - * - * @param string $methods Allowed methods, | delimited - * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @var \Neuron\Net\Request */ - public function before($methods, $pattern, $fn) { + private $request; - $pattern = $this->baseroute . '/' . trim($pattern, '/'); - $pattern = $this->baseroute ? rtrim($pattern, '/') : $pattern; + /** @var Module|null */ + private $module = null; - foreach (explode('|', $methods) as $method) { - $this->befores[$method][] = array( - 'pattern' => $pattern, - 'fn' => $fn - ); - } + /** @var [callable[],int] */ + private $filters = array (); + + /** + * @var Application + */ + private $app; + /** + * @param Application $app + */ + public function setApplication(Application $app) + { + $this->app = $app; } /** @@ -91,31 +71,44 @@ public function before($methods, $pattern, $fn) { * * @param string $methods Allowed methods, | delimited * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @param mixed $fn The handling function to be executed + * @return Route */ - public function match($methods, $pattern, $fn) { + public function match ($methods, $pattern, $fn) { $pattern = $this->baseroute . '/' . trim($pattern, '/'); $pattern = $this->baseroute ? rtrim($pattern, '/') : $pattern; + $route = new Route ($pattern); + $route->setFunction ($fn); + $route->setModule ($this->module); + foreach (explode('|', $methods) as $method) { - $this->routes[$method][] = array( - 'pattern' => $pattern, - 'fn' => $fn - ); + $this->routes[$method][] = $route; } + return $route; } + /** + * Set the module that will be used in the constructor for all Controllers + * that match the path of all future matches. + * @param Module $module + */ + private function setModule (Module $module = null) + { + $this->module = $module; + } /** * Shorthand for a route accessed using GET * * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @param mixed $fn The handling function to be executed + * @return Route */ public function get($pattern, $fn) { - $this->match('GET', $pattern, $fn); + return $this->match('GET', $pattern, $fn); } @@ -123,10 +116,11 @@ public function get($pattern, $fn) { * Shorthand for a route accessed using POST * * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @param mixed $fn The handling function to be executed + * @return Route */ public function post($pattern, $fn) { - $this->match('POST', $pattern, $fn); + return $this->match('POST', $pattern, $fn); } @@ -134,10 +128,11 @@ public function post($pattern, $fn) { * Shorthand for a route accessed using PATCH * * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @param mixed $fn The handling function to be executed + * @return Route */ public function patch($pattern, $fn) { - $this->match('PATCH', $pattern, $fn); + return $this->match('PATCH', $pattern, $fn); } @@ -145,10 +140,11 @@ public function patch($pattern, $fn) { * Shorthand for a route accessed using DELETE * * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @param mixed $fn The handling function to be executed + * @return Route */ public function delete($pattern, $fn) { - $this->match('DELETE', $pattern, $fn); + return $this->match('DELETE', $pattern, $fn); } @@ -156,238 +152,277 @@ public function delete($pattern, $fn) { * Shorthand for a route accessed using PUT * * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @param mixed $fn The handling function to be executed + * @return Route */ public function put($pattern, $fn) { - $this->match('PUT', $pattern, $fn); + return $this->match('PUT', $pattern, $fn); } - /** * Shorthand for a route accessed using OPTIONS * * @param string $pattern A route pattern such as /about/system - * @param object $fn The handling function to be executed + * @param mixed $fn The handling function to be executed + * @return Route */ public function options($pattern, $fn) { - $this->match('OPTIONS', $pattern, $fn); + return $this->match('OPTIONS', $pattern, $fn); } /** - * Mounts a collection of callables onto a base route - * - * @param string $baseroute The route subpattern to mount the callables on - * @param callable $fn The callabled to be called + * @param $prefix + * @param Interfaces\Module $module */ - public function mount($baseroute, $fn) { - - // Track current baseroute - $curBaseroute = $this->baseroute; - - // Build new baseroute string - $this->baseroute .= $baseroute; - - // Call the callable - call_user_func($fn); - - // Restore original baseroute - $this->baseroute = $curBaseroute; + public function module ($prefix, Module $module) + { + $module->initialize ($prefix); + $this->setModule ($module); + $module->setRoutes ($this, $prefix); + $this->setModule (null); } - /** - * Get all request headers - * @return array The request headers + * Execute the router: Loop all defined before middlewares and routes, and execute the handling function if a mactch was found + * + * @param Request $request */ - public function getRequestHeaders() { - - // getallheaders available, use that - if (function_exists('getallheaders')) return getallheaders(); - - // getallheaders not available: manually extract 'm - $headers = array(); - foreach ($_SERVER as $name => $value) { - if ((substr($name, 0, 5) == 'HTTP_') || ($name == 'CONTENT_TYPE') || ($name == 'CONTENT_LENGTH')) { - $headers[str_replace(array(' ', 'Http'), array('-', 'HTTP'), ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + public function run (Request $request) + { + try { + + // Define which method we need to handle + $this->method = $request->getMethod(); + + // Set request + $this->request = $request; + + // Handle all routes + $numHandled = 0; + if (isset($this->routes[$this->method])) + $numHandled = $this->handle($this->routes[$this->method], true); + + // If no route was handled, trigger the 404 (if any) + if ($numHandled == 0) { + if ($this->notFound) { + //call_user_func($this->notFound); + $this->handleMatch($this->notFound, array()); + } else { + $request = Response::error('Page not found.', Response::STATUS_NOTFOUND); + $request->output(); + } } - } - return $headers; - - } + // If it originally was a HEAD request, clean up after ourselves by emptying the output buffer + if ($_SERVER['REQUEST_METHOD'] == 'HEAD') ob_end_clean(); - /** - * Get the request method used, taking overrides into account - * @return string The Request method to handle - */ - public function getRequestMethod() { - - // Take the method as found in $_SERVER - $method = $_SERVER['REQUEST_METHOD']; - - // If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification - // @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4 - if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { - ob_start(); - $method = 'GET'; + } catch (ResponseException $e) { + $e->getResponse()->output(); } - - // If it's a POST request, check for a method override header - else if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $headers = $this->getRequestHeaders(); - if (isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], array('PUT', 'DELETE', 'PATCH'))) { - $method = $headers['X-HTTP-Method-Override']; - } - } - - return $method; - } - /** - * Execute the router: Loop all defined before middlewares and routes, and execute the handling function if a mactch was found - * - * @param object $callback Function to be executed after a matching route was handled (= after router middleware) + * During execution of the dispatch, this method will return the request. + * @return Request|null */ - public function run($callback = null) { - - // Define which method we need to handle - $this->method = $this->getRequestMethod(); - - // Handle all before middlewares - if (isset($this->befores[$this->method])) - $this->handle($this->befores[$this->method]); - - // Handle all routes - $numHandled = 0; - if (isset($this->routes[$this->method])) - $numHandled = $this->handle($this->routes[$this->method], true); - - // If no route was handled, trigger the 404 (if any) - if ($numHandled == 0) { - if ($this->notFound && is_callable($this->notFound)) call_user_func($this->notFound); - else header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); - } - // If a route was handled, perform the finish callback (if any) - else { - if ($callback && is_callable ($callback)) $callback(); - } - - // If it originally was a HEAD request, clean up after ourselves by emptying the output buffer - if ($_SERVER['REQUEST_METHOD'] == 'HEAD') ob_end_clean(); - + public function getRequest () + { + return $this->request; } - /** * Set the 404 handling function - * @param object $fn The function to be executed + * @param $fn + * @return Route|object */ public function set404($fn) { - $this->notFound = $fn; - } + $this->notFound = new Route ("404"); + $this->notFound->setFunction ($fn); + return $this->notFound; + } /** * Handle a a set of routes: if a match is found, execute the relating handling function * @param array $routes Collection of route patterns and their handling functions - * @param boolean $quitAfterRun Does the handle function need to quit after one route was matched? - * @return int The number of routes handled + * @throws InvalidParameter + * @return \Neuron\Net\Response The response */ - private function handle($routes, $quitAfterRun = false) { - - // Counter to keep track of the number of routes we've handled - $numHandled = 0; + private function handle ($routes) { // The current page URL - $uri = $this->getCurrentUri(); - - // Variables in the URL - $urlvars = array(); + $numHandled = 0; // Loop all routes foreach ($routes as $route) { - // we have a match! - if (preg_match_all('#^' . $route['pattern'] . '$#', $uri, $matches, PREG_OFFSET_CAPTURE)) { + if (!$route instanceof Route) + throw new InvalidParameter ("Route contains invalid models."); - // Rework matches to only contain the matches, not the orig string - $matches = array_slice($matches, 1); - - // Extract the matched URL parameters (and only the parameters) - $params = array_map(function($match, $index) use ($matches) { - - // We have a following parameter: take the substring from the current param position until the next one's position (thank you PREG_OFFSET_CAPTURE) - if (isset($matches[$index+1]) && isset($matches[$index+1][0]) && is_array($matches[$index+1][0])) { - return trim(substr($match[0][0], 0, $matches[$index+1][0][1] - $match[0][1]), '/'); - } - - // We have no following paramete: return the whole lot - else { - return (isset($match[0][0]) ? trim($match[0][0], '/') : null); - } + // we have a match! + if ($params = $this->request->parseRoute ($route)) { - }, $matches, array_keys($matches)); + if (!is_array ($params)) { + $params = array (); + } // call the handling function with the URL parameters - $this->handleOutput (call_user_func_array($route['fn'], $params)); + $this->handleMatch ($route, $params); + //call_user_func_array($route['fn'], $params); // yay! - $numHandled++; + $numHandled ++; // If we need to quit, then quit - if ($quitAfterRun) break; + //if ($quitAfterRun) break; } } - // Return the number of routes handled return $numHandled; } - private function handleOutput ($output) + /** + * Add a filter that can be added. + * @param string $filtername + * @param callable $method + * @param int $priority + */ + public function addFilter ($filtername, callable $method = null, $priority = 0) { - if (!$output) - { - // Nothing to do. - return; + $this->filters[$filtername] = [ + 'callback' => $method, + 'priority' => $priority + ]; + } + + /** + * @param Route $route + * @param $params + * @throws InvalidParameter + */ + private function handleMatch (Route $route, $params) + { + $function = $route->getFunction (); + + // Check for additional parameters + foreach ($route->getParameters () as $v) { + $params[] = $v; } - if ($output instanceof \Neuron\Net\Response) + // First handle the filters + + // Find all filters that need to be execued + $filtersToExecute = []; + + foreach ($route->getFilters () as $filter) { - $output->output (); + // Check if exist + if (!isset ($this->filters[$filter->getName ()])) { + throw new InvalidParameter ("Filter " . $filter->getName() . " is not registered in the router."); + } + + $filterCallback = $this->filters[$filter->getName()]; + + $filtersToExecute[] = [ + 'callback' => $filterCallback['callback'], + 'priority' => $filterCallback['priority'], + 'filter' => $filter + ]; } + // order filters on priority + usort($filtersToExecute, function($a, $b) { + if ($a['priority'] == $b['priority']) { + return 0; + } + return $a['priority'] < $b['priority'] ? 1 : -1; + }); + + foreach ($filtersToExecute as $v) { + + $filter = $v['filter']; + + $filter->setRequest ($this->request); + $response = $filter->check ($v['callback'], $params); + $filter->clearRequest (); + + // If output was not TRUE, handle the filter return value as output. + if ($response !== true) { + $this->output ($response); + return; + } + } + + // trigger post filter events + $this->app->postFilters($this->request); + + if (is_callable ($function)) + { + $response = call_user_func_array($function, $params); + } else { - echo $output; + if (strpos ($function, '@')) + { + $param = explode ('@', $function); + if (count ($param) !== 2) + { + throw new InvalidParameter ("Controller@method syntax not valid for " . $function); + } + + $response = $this->handleController ($param[0], $param[1], $params, $route->getModule ()); + } + else { + throw new InvalidParameter ("Method not found."); + } } + + $this->output ($response); } + private function output ($response) + { + if ($response) + { + if ($response instanceof Response) + { + $response->output (); + } + else { + echo $response; + } + } + } /** - * Define the current relative URI - * @return string + * @param string $controller + * @param string $method + * @param array $params + * @param Module $module + * @throws Exceptions\DataNotFound + * @throws InvalidParameter + * @return mixed */ - private function getCurrentUri() { - - // Get the current Request URI and remove rewrite basepath from it (= allows one to run the router in a subfolder) - $basepath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/'; - $uri = substr($_SERVER['REQUEST_URI'], strlen($basepath)); - - // Don't take query params into account on the URL - if (strstr($uri, '?')) $uri = substr($uri, 0, strpos($uri, '?')); - - // Remove trailing slash + enforce a slash at the start - $uri = '/' . trim($uri, '/'); + private function handleController ($controller, $method, $params, $module = null) + { + $controller = ControllerFactory::getInstance ()->getController ($controller, $module); - return $uri; + // If the found controller implements the Controller interface, we set the request. + if ($controller instanceof Controller) + { + $controller->setRequest ($this->request); + } + if (is_callable (array ($controller, $method))) + { + return call_user_func_array(array ($controller, $method), $params); + } + else { + throw new InvalidParameter ("Method not found."); + } } } - -// EOF \ No newline at end of file diff --git a/src/Neuron/Session.php b/src/Neuron/Session.php deleted file mode 100644 index 15ce6a8..0000000 --- a/src/Neuron/Session.php +++ /dev/null @@ -1,205 +0,0 @@ -handler)) - { - $this->handler = new \Neuron\SessionHandlers\DbSessionHandler (); - $this->handler->register (); - } - $this->handler->start ($sessionId); - - //$this->handler = $handler; - - /* - if (isset($_GET['session_id'])) - { - session_id($_GET['session_id']); - } - - session_start (); - */ - } - - public function disconnect () - { - if ($this->handler) - { - $this->handler->stop (); - } - } - - /** - * Destroy a session - */ - public function destroy () - { - // Unset all of the session variables. - $_SESSION = array(); - - // If it's desired to kill the session, also delete the session cookie. - // Note: This will destroy the session, and not just the session data! - if (ini_get("session.use_cookies")) { - $params = session_get_cookie_params(); - setcookie(session_name(), '', time() - 42000, - $params["path"], $params["domain"], - $params["secure"], $params["httponly"] - ); - } - - // Finally, destroy the session. - session_destroy(); - } - - public function setUser (User $user) - { - $this->user = $user; - } - - public function isLogin () - { - return isset ($_SESSION['user']); - } - - private function loadUser () - { - if (!isset ($this->user)) - { - if (isset ($_SESSION['user'])) - { - $this->setUser (MapperFactory::getUserMapper()->getFromId ($_SESSION['user'])); - } - else - { - $this->user = false; - } - } - } - - /** - * @return \Neuron\Models\User - */ - public function getUser() - { - $this->loadUser (); - return $this->user; - } - - public function login (User $user) - { - $_SESSION['user'] = $user->getId (); - $this->user = $user; - - $_SESSION['user_admin_status'] = $user->getAdminRights (); - - // Events! - EventManager::getInstance ()->trigger ('signin:login', $user); - - return $user; - } - - public function logout ($destroySession = true) - { - $_SESSION['user'] = null; - $this->user = null; - - EventManager::getInstance ()->trigger ('signin:logout'); - - if ($destroySession) - { - $this->destroy (); - } - } - - public function register (User $user) - { - // Create the user - $user = MapperFactory::getUserMapper()->create ($user); - - EventManager::getInstance ()->trigger ('signin:register', $user); - - // Login the user - $this->login ($user); - - return $user; - } - - public function isEmailUnique ($email) - { - $user = MapperFactory::getUserMapper()->getFromEmail ($email); - if ($user) - { - return false; - } - return true; - } - - public function setApplication ($application) - { - $this->application = $application; - } - - public function getApplication () - { - return $this->application; - } - - public function set ($key, $value) - { - $this->values[$key] = $value; - } - - public function get ($key) - { - if (isset ($this->values[$key])) - { - return $this->values[$key]; - } - return null; - } -} \ No newline at end of file diff --git a/src/Neuron/SessionHandlers/DbSessionHandler.php b/src/Neuron/SessionHandlers/DbSessionHandler.php index e6a130c..5d49f50 100644 --- a/src/Neuron/SessionHandlers/DbSessionHandler.php +++ b/src/Neuron/SessionHandlers/DbSessionHandler.php @@ -8,12 +8,13 @@ namespace Neuron\SessionHandlers; +use Neuron\DB\Database; use Neuron\DB\Query; class DbSessionHandler extends SessionHandler { - /** @var \Neuron\DB\Database $db */ + /** @var Database $db */ private $db; private $sessions = array (); @@ -21,7 +22,7 @@ class DbSessionHandler public function open ( $save_path , $name ) { // Force loading of query. - $this->db = \Neuron\DB\Database::getInstance (); + $this->db = Database::getInstance (); // Nothing to do. return true; @@ -56,7 +57,7 @@ public function read ( $session_id ) } else { - $this->sessions[$session_id] = null; + $this->sessions[$session_id] = ''; } } return $this->sessions[$session_id]; diff --git a/src/Neuron/SessionHandlers/SessionHandler.php b/src/Neuron/SessionHandlers/SessionHandler.php index 7cb49f5..86896a5 100644 --- a/src/Neuron/SessionHandlers/SessionHandler.php +++ b/src/Neuron/SessionHandlers/SessionHandler.php @@ -1,57 +1,45 @@ started) { - /* - var_dump ($_COOKIE); - var_dump ($_REQUEST); - */ - - if (isset($sessionId)) - { - session_id ($sessionId); - - ini_set("session.use_cookies",0); - ini_set("session.use_only_cookies",0); - ini_set("session.use_trans_sid",0); # Forgot this one! - - Logger::getInstance()->log ("Starting session with provided id " . $sessionId, false, 'cyan'); - } - - else if ($defaultSession = Tools::getInput ($_COOKIE, 'PHPSESSID', 'varchar')) - { - Logger::getInstance()->log ("Starting session with default cookie " . $defaultSession, false, 'cyan'); - session_id ($defaultSession); - } - - else - { - /** - * @TODO check if this is proper - * - * We're using uniqid to set the session id. - */ - $sid = uniqid ('session', true); - session_id ($sid); - Logger::getInstance()->log ("Starting brand new session with id " . $sid, false, 'cyan'); - } + $this->register (); + + if (isset($sessionId)) { + session_id ($sessionId); + + ini_set ("session.use_cookies", 0); + ini_set ("session.use_only_cookies", 0); + ini_set ("session.use_trans_sid", 0); # Forgot this one! + + Logger::getInstance ()->log ("Starting session with provided id " . $sessionId, false, 'cyan'); + } elseif ($defaultSession = Tools::getInput ($_COOKIE, 'PHPSESSID', 'varchar')) { + Logger::getInstance()->log("Starting session with default cookie " . $defaultSession, false, 'cyan'); + session_id($defaultSession); + } elseif ($queryParamSession = Tools::getInput ($_GET, self::SESSION_QUERY_PARAMETER, 'varchar')) { + Logger::getInstance()->log("Starting session with query parameter " . $queryParamSession, false, 'cyan'); + session_id($queryParamSession); + } elseif (session_status() == PHP_SESSION_ACTIVE) { + session_regenerate_id (); + Logger::getInstance ()->log ("Starting brand new session with id " . session_id (), false, 'cyan'); + } session_start (); @@ -61,22 +49,55 @@ public final function start ($sessionId = null) public final function stop () { - Logger::getInstance()->log ("Closing session with id " . session_id (), false, 'cyan'); + Logger::getInstance ()->log ("Closing session with id " . session_id (), false, 'cyan'); session_write_close (); $this->started = false; } + /** + * @return string + */ + public function getSessionQueryString() + { + return self::SESSION_QUERY_PARAMETER . '=' . session_id(); + } + /* Methods */ - abstract public function close ( ); - abstract public function destroy ( $session_id ); - abstract public function gc ( $maxlifetime ); - abstract public function open ( $save_path , $name ); - abstract public function read ( $session_id ); - abstract public function write ( $session_id , $session_data ); + public function close () + { + return parent::close (); + } + + public function destroy ($session_id) + { + return parent::destroy ($session_id); + } + + public function gc ( $maxlifetime ) + { + return parent::gc ($maxlifetime); + } + + public function open ( $save_path , $name ) + { + return parent::open ( $save_path, $name ); + } + + public function read ( $session_id ) + { + return parent::read ($session_id); + } + + + public function write ( $session_id , $session_data ) + { + return parent::write ($session_id, $session_data); + } public function register () { + session_set_save_handler( array($this, 'open'), array($this, 'close'), @@ -86,4 +107,4 @@ public function register () array($this, 'gc') ); } -} \ No newline at end of file +} diff --git a/src/Neuron/Tests/SyntaxTest.php b/src/Neuron/Tests/SyntaxTest.php deleted file mode 100644 index 3781c40..0000000 --- a/src/Neuron/Tests/SyntaxTest.php +++ /dev/null @@ -1,57 +0,0 @@ -scanDirectory ($directory . '/' . $v); - } - - elseif (is_file ($directory . '/' . $v)) - { - // Check the file - $result = $this->isValidPHP ($directory .'/' . $v); - - $this->assertTrue ($result); - if (!$result) - { - echo "Syntax error found in " . $directory . '/' . $v . "\n\n\n"; - } - } - } - } - - public function testCodeSyntax () - { - $this->scanDirectory (dirname (dirname (__FILE__))); - } -} \ No newline at end of file diff --git a/src/Neuron/Tests/ToolsTest.php b/src/Neuron/Tests/ToolsTest.php deleted file mode 100644 index 11af55e..0000000 --- a/src/Neuron/Tests/ToolsTest.php +++ /dev/null @@ -1,100 +0,0 @@ -assertTrue (Tools::checkInput ('thijs@catlab.be', 'email')); - $this->assertTrue (Tools::checkInput ('thijs.vanderschaeghe@catlab.be', 'email')); - - // Invalid email address - $this->assertFalse (Tools::checkInput (0, 'email')); - $this->assertFalse (Tools::checkInput (null, 'email')); - $this->assertFalse (Tools::checkInput (false, 'email')); - $this->assertFalse (Tools::checkInput ('thijs', 'email')); - $this->assertFalse (Tools::checkInput ('@catlab.be', 'email')); - $this->assertFalse (Tools::checkInput ('thijs@home@catlab.be', 'email')); - } - - public function testURLInputCheck () - { - //$this->assertTrue (Tools::checkInput ('huffingtonpost.com/2014/06/13/iraq-defend-country_n_5491357.html?1402661760', 'url')); - - $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.catlab.eu', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.catlab.eu?foo=bar&bla=bam', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/?foo=bar&bla=bam', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/index.html?foo=bar&bla=bam', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/index.php?foo=bar&bla=bam', 'url')); - - $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/', 'url')); - $this->assertTrue (Tools::checkInput ('https://www.catlab.eu', 'url')); - $this->assertTrue (Tools::checkInput ('https://www.catlab.eu?foo=bar&bla=bam', 'url')); - $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/?foo=bar&bla=bam', 'url')); - $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/index.html?foo=bar&bla=bam', 'url')); - $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/index.php?foo=bar&bla=bam', 'url')); - $this->assertTrue (Tools::checkInput ('http://socialmouths.com/blog/2014/01/24/google-plus-features/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+Socialmouths+%28SocialMouths%29', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.business2community.com/social-media/social-media-strategy-wont-work-without-one-thing-0911103#!YluUO', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.latimes.com/world/middleeast/la-fg-obama-iraq-20140613-story.html#page=1', 'url')); - $this->assertTrue (Tools::checkInput ('http://www.huffingtonpost.com/2014/06/13/iraq-defend-country_n_5491357.html?1402661760', 'url')); - $this->assertTrue (Tools::checkInput ('ww.link.be', 'url')); - - $this->assertTrue (Tools::checkInput ('www.huffingtonpost.com/2014/06/13/iraq-defend-country_n_5491357.html?1402661760', 'url')); - - $this->assertFalse (Tools::checkInput ('this is not an url.', 'url')); - $this->assertFalse (Tools::checkInput ('thisisalsonotanurl.', 'url')); - $this->assertFalse (Tools::checkInput ('.neitheristhis', 'url')); - $this->assertFalse (Tools::checkInput ('.or this', 'url')); - - //$this->assertFalse (Tools::checkInput ('iwouldliketobeanurl.but im not', 'url')); - - $this->assertFalse (Tools::checkInput ('test', 'url')); -// $this->assertFalse (Tools::checkInput ('w.test', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('w.test.com', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('ftp://user:password@domain.com/path/', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('https://www.test.subdomain.domain.xyz/', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('domain.test/#anchor', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('domain.co/?query=123', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('mailto://user@unkwn.com', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('http://www.domain.co/path/to/index.ext', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('http://www.domain.co\path\to\stuff.txt', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('http://www.domain.co\path@to#stuff$txt', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('www.test.com/file[/]index.html', 'url')); // @TODO -// $this->assertFalse (Tools::checkInput ('www.test.com/file{/}index.html', 'url')); // @TODO - $this->assertFalse (Tools::checkInput ('www."test".com', 'url')); - } - - public function testNumberInput () - { - $this->assertTrue (Tools::checkInput (5, 'number')); - $this->assertTrue (Tools::checkInput (5.0, 'number')); - $this->assertTrue (Tools::checkInput ('5.0', 'number')); - - $this->assertFalse (Tools::checkInput ('five', 'number')); - $this->assertFalse (Tools::checkInput ('23,5', 'number')); - $this->assertFalse (Tools::checkInput ('foobaaaar', 'number')); - } - - public function testIntInput () - { - $this->assertTrue (Tools::checkInput (5, 'int')); - $this->assertTrue (Tools::checkInput (5.0, 'int')); - $this->assertTrue (Tools::checkInput ('5', 'int')); - $this->assertTrue (Tools::checkInput ('5.0', 'int')); - - $this->assertFalse (Tools::checkInput (5.1, 'int')); - $this->assertFalse (Tools::checkInput ('5.1', 'int')); - $this->assertFalse (Tools::checkInput ('foobar', 'int')); - $this->assertFalse (Tools::checkInput ('23,5', 'int')); - - } -} \ No newline at end of file diff --git a/src/Neuron/Tools/ControllerFactory.php b/src/Neuron/Tools/ControllerFactory.php new file mode 100644 index 0000000..b08ea76 --- /dev/null +++ b/src/Neuron/Tools/ControllerFactory.php @@ -0,0 +1,53 @@ +controllers[$name])) { + if (class_exists ($name)) { + $this->controllers[$name] = new $name ($module); + } else { + throw new DataNotFound ("Controller not found: " . $name); + } + } + + return $this->controllers[$name]; + } +} \ No newline at end of file diff --git a/src/Neuron/Core/Memcache.php b/src/Neuron/Tools/Memcache.php similarity index 97% rename from src/Neuron/Core/Memcache.php rename to src/Neuron/Tools/Memcache.php index 73972dd..eae4b44 100644 --- a/src/Neuron/Core/Memcache.php +++ b/src/Neuron/Tools/Memcache.php @@ -1,6 +1,6 @@ domain = $domain; + return $this; + } + + /** + * @param string $message1 + * @param string|null $message2 + * @param int|null $n + * @return string + */ + public function getText ($message1, $message2 = null, $n = null) + { + if (!isset ($message2)) { + return dgettext ($this->domain, $message1); + } + else { + return dngettext ($this->domain, $message1, $message2, $n); + } + } + + /** + * Little helper method. + * @param string $message1 + * @param string|null $message2 + * @param string|null $n + * @return string + */ + public static function get ($message1, $message2 = null, $n = null) + { + $in = self::getInstance (); + return $in->getText ($message1, $message2, $n); + } +} diff --git a/src/Neuron/Tools/TokenGenerator.php b/src/Neuron/Tools/TokenGenerator.php new file mode 100644 index 0000000..e1bc0ee --- /dev/null +++ b/src/Neuron/Tools/TokenGenerator.php @@ -0,0 +1,38 @@ + $v) + if (!isset ($appurl)) + $appurl = Config::get ('app.url', '/'); + + if (substr ($module, 0, 1) === '/') { - $params .= urlencode ($k) . '=' . urlencode ($v) . '&'; + $module = substr ($module, 1); + } + + $params = ''; + if (isset ($data)) { + foreach ($data as $k => $v) { + $params .= urlencode ($k) . '=' . urlencode ($v) . '&'; + } } $params = substr ($params, 0, -1); if (!empty ($params)) { - return BASE_URL . $module . '?' . $params; + if ($normalize) + $url = self::normalize ($appurl . $module) . '?' . $params; + else + $url = $appurl . $module . '?' . $params; + + return $url; } else { // Google likes these. - return BASE_URL . $module; + if ($normalize) + return self::normalize ($appurl . $module); + else + return $appurl . $module; + } + } + + public static function getAbsoluteURL ($module = '', $data = array ()) { + + if (self::isAbsolute (Config::get ('app.url'))) { + return self::getURL ($module, $data, true); + } + else { + return self::getURL ($module, $data, true, self::guessAbsoluteURL ()); + } + } + + /** + * Guest absolute url from server variables. + * @return string + */ + private static function guessAbsoluteURL () { + $scheme = 'http'; + $host = 'localhost'; + + if (isset($_SERVER['REQUEST_SCHEME'])) { + $scheme = $_SERVER['REQUEST_SCHEME']; + } + + if (isset($_SERVER['HTTP_HOST'])) { + $host = $_SERVER['HTTP_HOST']; + } + + return $scheme . '://' . $host . '/'; + } + + private static function isAbsolute ($url) { + if (substr (strtolower ($url), 0, 4) == 'http') { + return true; + } + return false; + } + + /** + * Make sure the string does not end with a / + * @param $path + * @return string + */ + public static function normalize ($path) + { + if (substr ($path, 0, -1) === '/') + { + return substr ($path, 0, -1); + } + return $path; + } + + /** + * Make sure that the string ends with a slash. + * @param string $path + * @return string + */ + public static function partify ($path) + { + if (substr ($path, -1) !== '/') + { + return $path . '/'; } + return $path; } } \ No newline at end of file diff --git a/src/templates/error.phpt b/src/templates/error.phpt new file mode 100644 index 0000000..a28849e --- /dev/null +++ b/src/templates/error.phpt @@ -0,0 +1,27 @@ + + + + + + css ('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css'); ?> + + + css ('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap-theme.min.css'); ?> + + + js ('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js'); ?> + + + + + +
+ +
+

+

+
+
+ + + \ No newline at end of file diff --git a/src/templates/index.phpt b/src/templates/index.phpt new file mode 100644 index 0000000..3916b6e --- /dev/null +++ b/src/templates/index.phpt @@ -0,0 +1,14 @@ + + + + + combine ('sections/head.phpt'); ?> + + + + + + + + + \ No newline at end of file diff --git a/src/templates/sections/head.phpt b/src/templates/sections/head.phpt new file mode 100644 index 0000000..2362ed9 --- /dev/null +++ b/src/templates/sections/head.phpt @@ -0,0 +1,3 @@ + + + diff --git a/tests/CollectionTest.php b/tests/CollectionTest.php new file mode 100644 index 0000000..b13d453 --- /dev/null +++ b/tests/CollectionTest.php @@ -0,0 +1,195 @@ +assertCount (0, $collection); + + $collection->add ('a'); + $collection->add ('b'); + $collection->add ('c'); + $this->assertCount (3, $collection); + } + + public function testFirstAndLast () + { + $collection = new Collection (); + $collection->add ('first'); + $collection->add ('middle'); + $collection->add ('last'); + + $this->assertEquals ('first', $collection->first ()); + $this->assertEquals ('last', $collection->last ()); + } + + public function testFirstAndLastEmpty () + { + $collection = new Collection (); + $this->assertNull ($collection->first ()); + $this->assertNull ($collection->last ()); + } + + public function testIterator () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + $collection->add ('c'); + + $values = []; + foreach ($collection as $key => $value) { + $values[$key] = $value; + } + + $this->assertEquals ([0 => 'a', 1 => 'b', 2 => 'c'], $values); + } + + public function testRewind () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + + // Iterate to end + foreach ($collection as $v) {} + + // Rewind and verify + $collection->rewind (); + $this->assertEquals ('a', $collection->current ()); + } + + public function testArrayAccess () + { + $collection = new Collection (); + $collection[] = 'value1'; + $collection[] = 'value2'; + + $this->assertTrue (isset ($collection[0])); + $this->assertTrue (isset ($collection[1])); + $this->assertFalse (isset ($collection[2])); + + $this->assertEquals ('value1', $collection[0]); + $this->assertEquals ('value2', $collection[1]); + } + + public function testOffsetSet () + { + $collection = new Collection (); + $collection[5] = 'value5'; + + $this->assertTrue (isset ($collection[5])); + $this->assertEquals ('value5', $collection[5]); + } + + public function testOffsetUnset () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + + unset ($collection[0]); + $this->assertFalse (isset ($collection[0])); + } + + public function testRemove () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + $collection->add ('c'); + + $result = $collection->remove ('b'); + $this->assertTrue ($result); + $this->assertCount (2, $collection); + } + + public function testRemoveNonExistent () + { + $collection = new Collection (); + $collection->add ('a'); + + $result = $collection->remove ('nonexistent'); + $this->assertFalse ($result); + } + + public function testClear () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + + $collection->clear (); + $this->assertCount (0, $collection); + } + + public function testPeek () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + $collection->add ('c'); + + $collection->rewind (); + $this->assertEquals ('a', $collection->current ()); + $this->assertEquals ('b', $collection->peek ()); + // Position should not have changed + $this->assertEquals ('a', $collection->current ()); + } + + public function testPeekAtEnd () + { + $collection = new Collection (); + $collection->add ('a'); + + $collection->rewind (); + $this->assertNull ($collection->peek ()); + } + + public function testReverse () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + $collection->add ('c'); + + $collection->reverse (); + $this->assertEquals ('c', $collection->first ()); + $this->assertEquals ('a', $collection->last ()); + } + + public function testCurrentAtInvalidPosition () + { + $collection = new Collection (); + $this->assertNull ($collection->current ()); + } + + public function testValid () + { + $collection = new Collection (); + $collection->add ('a'); + + $collection->rewind (); + $this->assertTrue ($collection->valid ()); + $collection->next (); + $this->assertFalse ($collection->valid ()); + } + + public function testKey () + { + $collection = new Collection (); + $collection->add ('a'); + $collection->add ('b'); + + $collection->rewind (); + $this->assertEquals (0, $collection->key ()); + $collection->next (); + $this->assertEquals (1, $collection->key ()); + } +} diff --git a/tests/DbQueryInjectionTest.php b/tests/DbQueryInjectionTest.php new file mode 100644 index 0000000..a4a82a7 --- /dev/null +++ b/tests/DbQueryInjectionTest.php @@ -0,0 +1,862 @@ +bindValue (1, "O'Reilly", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// Single quote must be escaped as \' +$this->assertStringContainsString ("\\'", $sql); +// The value is wrapped in outer single quotes +$this->assertStringContainsString ("name = '", $sql); +} + +public function testStringParamEscapesDoubleQuote () +{ +$query = new Query ("SELECT * FROM `users` WHERE name = ?"); +$query->bindValue (1, 'Say "hello"', Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('\\"', $sql); +} + +public function testStringParamEscapesBackslash () +{ +$query = new Query ("SELECT * FROM `users` WHERE path = ?"); +$query->bindValue (1, 'C:\\Users\\test', Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('\\\\', $sql); +} + +public function testStringParamEscapesNewline () +{ +$query = new Query ("SELECT * FROM `users` WHERE note = ?"); +$query->bindValue (1, "line1\nline2", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('\\n', $sql); +} + +public function testStringParamEscapesNullByte () +{ +$query = new Query ("SELECT * FROM `users` WHERE name = ?"); +$query->bindValue (1, "name\x00injected", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('\\0', $sql); +} + +public function testClassicOrInjectionInString () +{ +$query = new Query ("SELECT * FROM `users` WHERE username = ?"); +$query->bindValue (1, "' OR '1'='1", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// The dangerous ' is escaped to \' preventing context break-out +$this->assertStringContainsString ("\\'", $sql); +// Verify the whole value is inside outer quotes +$this->assertStringContainsString ("username = '", $sql); +} + +public function testDropTableInjectionInString () +{ +$query = new Query ("INSERT INTO `log` SET message = ?"); +$query->bindValue (1, "'; DROP TABLE users;--", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// The leading ' is escaped so the payload cannot break out of the string context +$this->assertStringContainsString ("\\'", $sql); +$this->assertStringContainsString ("message = '", $sql); +} + +public function testUnionSelectInjectionInString () +{ +$query = new Query ("SELECT * FROM `products` WHERE name = ?"); +$query->bindValue (1, "' UNION SELECT username, password FROM users--", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// The single quote is escaped; the UNION cannot be executed as SQL +$this->assertStringContainsString ("\\'", $sql); +} + +public function testSleepInjectionInString () +{ +$query = new Query ("SELECT * FROM `users` WHERE id = ?"); +$query->bindValue (1, "1' AND SLEEP(5)--", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// The single quote is escaped +$this->assertStringContainsString ("\\'", $sql); +$this->assertStringContainsString ("id = '", $sql); +} + +public function testStackedQueryInjection () +{ +$query = new Query ("SELECT * FROM `users` WHERE name = ?"); +$query->bindValue (1, "admin'; SELECT * FROM secrets;--", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// Quote escaped; second statement cannot execute +$this->assertStringContainsString ("\\'", $sql); +} + +public function testCommentBasedInjectionInString () +{ +$query = new Query ("SELECT * FROM `users` WHERE name = ?"); +$query->bindValue (1, "admin'--", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// Single quote must be escaped +$this->assertStringContainsString ("admin\\'--", $sql); +} + +public function testBlindInjectionInString () +{ +$query = new Query ("SELECT * FROM `users` WHERE name = ?"); +$query->bindValue (1, "' AND 1=1--", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +// Single quote escaped +$this->assertStringContainsString ("\\'", $sql); +} + +public function testNumericValueInStringParam () +{ +$query = new Query ("SELECT * FROM `t` WHERE col = ?"); +$query->bindValue (1, 42, Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("'42'", $sql); +} + +public function testFloatValueInStringParam () +{ +$query = new Query ("SELECT * FROM `t` WHERE col = ?"); +$query->bindValue (1, 3.14, Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("'3.14'", $sql); +} + +// --------------------------------------------------------------- +// PARAM_UNKNOWN — automatic type detection +// --------------------------------------------------------------- + +public function testUnknownParamIntIsNotQuoted () +{ +$query = new Query ("SELECT * FROM `t` WHERE id = ?"); +$query->bindValue (1, 5, Query::PARAM_UNKNOWN); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("id = 5", $sql); +$this->assertStringNotContainsString ("id = '5'", $sql); +} + +public function testUnknownParamStringInjection () +{ +$query = new Query ("SELECT * FROM `t` WHERE name = ?"); +$query->bindValue (1, "' OR '1'='1", Query::PARAM_UNKNOWN); +$sql = $query->getParsedQuery (); + +// Single quote escaped +$this->assertStringContainsString ("\\'", $sql); +} + +public function testUnknownParamCommaStringNotModified () +{ +// Comma-format "3,14" is not is_numeric(), treated as string +$query = new Query ("SELECT * FROM `t` WHERE val = ?"); +$query->bindValue (1, "3,14", Query::PARAM_UNKNOWN); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("'3,14'", $sql); +} + +// --------------------------------------------------------------- +// PARAM_NUMBER — numeric parameter protection +// --------------------------------------------------------------- + +public function testNumberParamValidInt () +{ +$query = new Query ("SELECT * FROM `t` WHERE id = ?"); +$query->bindValue (1, 42, Query::PARAM_NUMBER); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("id = 42", $sql); +} + +public function testNumberParamValidFloat () +{ +$query = new Query ("SELECT * FROM `t` WHERE price = ?"); +$query->bindValue (1, 9.99, Query::PARAM_NUMBER); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("price = 9.99", $sql); +} + +public function testNumberParamThrowsOnNonNumericString () +{ +$this->expectException (InvalidParameter::class); + +$query = new Query ("SELECT * FROM `t` WHERE id = ?"); +$query->bindValue (1, "'; DROP TABLE users;--", Query::PARAM_NUMBER); +$query->getParsedQuery (); +} + +public function testNumberParamThrowsOnInjectionString () +{ +$this->expectException (InvalidParameter::class); + +$query = new Query ("SELECT * FROM `t` WHERE id = ?"); +$query->bindValue (1, "1 OR 1=1", Query::PARAM_NUMBER); +$query->getParsedQuery (); +} + +public function testNumberParamThrowsOnUnionSelect () +{ +$this->expectException (InvalidParameter::class); + +$query = new Query ("SELECT * FROM `t` WHERE id = ?"); +$query->bindValue (1, "1 UNION SELECT password FROM users", Query::PARAM_NUMBER); +$query->getParsedQuery (); +} + +public function testNumberParamThrowsOnAlphaString () +{ +$this->expectException (InvalidParameter::class); + +$query = new Query ("SELECT * FROM `t` WHERE id = ?"); +$query->bindValue (1, "admin", Query::PARAM_NUMBER); +$query->getParsedQuery (); +} + +// --------------------------------------------------------------- +// PARAM_DATE +// --------------------------------------------------------------- + +public function testDateParamTimestamp () +{ +$ts = gmmktime (0, 0, 0, 6, 15, 2020); + +$query = new Query ("SELECT * FROM `t` WHERE created = ?"); +$query->bindValue (1, $ts, Query::PARAM_DATE); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("FROM_UNIXTIME($ts)", $sql); +} + +public function testDateParamDateTimeObject () +{ +$dt = new DateTime ('2020-06-15 12:30:00'); + +$query = new Query ("SELECT * FROM `t` WHERE created = ?"); +$query->bindValue (1, $dt, Query::PARAM_DATE); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("'2020-06-15 12:30:00'", $sql); +} + +public function testDateParamThrowsOnInjectionString () +{ +$this->expectException (InvalidParameter::class); + +$query = new Query ("SELECT * FROM `t` WHERE created = ?"); +$query->bindValue (1, "' OR '1'='1", Query::PARAM_DATE); +$query->getParsedQuery (); +} + +public function testDateParamThrowsOnNonNumericString () +{ +$this->expectException (InvalidParameter::class); + +$query = new Query ("SELECT * FROM `t` WHERE created = ?"); +$query->bindValue (1, "not-a-date", Query::PARAM_DATE); +$query->getParsedQuery (); +} + +// --------------------------------------------------------------- +// PARAM_POINT +// --------------------------------------------------------------- + +public function testPointParam () +{ +$point = new Point (4.3517, 50.8503); + +$query = new Query ("INSERT INTO `locations` SET pos = ?"); +$query->bindValue (1, $point, Query::PARAM_POINT); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("POINT(4.3517,50.8503)", $sql); +} + +public function testPointParamThrowsOnNonPoint () +{ +$this->expectException (InvalidParameter::class); + +$query = new Query ("INSERT INTO `locations` SET pos = ?"); +$query->bindValue (1, "POINT(0,0) INJECTION", Query::PARAM_POINT); +$query->getParsedQuery (); +} + +public function testPointConstructorRejectsNonNumeric () +{ +$this->expectException (InvalidParameter::class); +new Point ("x", "y"); +} + +// --------------------------------------------------------------- +// NULL handling +// --------------------------------------------------------------- + +public function testNullValueWithCanBeNullTrue () +{ +// null value + canBeNull=true → should produce NULL in SQL +$query = new Query ("UPDATE `t` SET col = ?"); +$query->bindValue (1, null, Query::PARAM_STR, true); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("col = NULL", $sql); +} + +public function testNullValueWithCanBeNullFalse () +{ +// null value + canBeNull=false (default) → treated as empty string +$query = new Query ("UPDATE `t` SET col = ?"); +$query->bindValue (1, null, Query::PARAM_STR, false); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("col = ''", $sql); +} + +public function testNullInWhereProducesIsNull () +{ +$query = Query::select ('users', array ('id'), array ('deleted_at' => null)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("deleted_at IS NULL", $sql); +} + +public function testNullViaStaticBuilderProducesNull () +{ +// Via static builder, no explicit type → $v[2] not set → defaults to true +// in getParsedQuery → null produces NULL +$query = Query::insert ('t', array ('col' => null)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("col = NULL", $sql); +} + +// --------------------------------------------------------------- +// Auto-detection of DateTime and Point (via bindValues without type) +// --------------------------------------------------------------- + +public function testAutoDetectDateTimeViaBindValues () +{ +$dt = new DateTime ('2023-01-15 08:00:00'); + +$query = new Query ("INSERT INTO `t` SET created = ?"); +$query->bindValues (array (array ($dt))); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("'2023-01-15 08:00:00'", $sql); +} + +public function testAutoDetectPointViaBindValues () +{ +$point = new Point (10.5, 20.3); + +$query = new Query ("INSERT INTO `t` SET pos = ?"); +$query->bindValues (array (array ($point))); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("POINT(10.5,20.3)", $sql); +} + +// --------------------------------------------------------------- +// Array (IN clause) values +// --------------------------------------------------------------- + +public function testArrayValueForInClause () +{ +$query = Query::select ('users', array ('id', 'name'), array ( +'id' => array (array (1, 2, 3), Query::PARAM_NUMBER), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("(1,2,3)", $sql); +} + +public function testArrayStringValuesEscaped () +{ +$query = Query::select ('users', array ('name'), array ( +'name' => array (array ("admin", "' OR '1'='1"), Query::PARAM_STR), +)); +$sql = $query->getParsedQuery (); + +// The dangerous single quote in the injection payload must be escaped +$this->assertStringContainsString ("\\'", $sql); +} + +// --------------------------------------------------------------- +// WHERE comparators +// --------------------------------------------------------------- + +public function testWhereNotEqualsPrefix () +{ +$query = Query::select ('t', array (), array ( +'status' => array ('!active', Query::PARAM_STR), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("status != ", $sql); +$this->assertStringContainsString ("'active'", $sql); +} + +public function testWhereLike () +{ +$query = Query::select ('t', array (), array ( +'name' => array ('%test%', Query::PARAM_STR, 'LIKE'), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("name LIKE ", $sql); +$this->assertStringContainsString ("'%test%'", $sql); +} + +public function testWhereNot () +{ +$query = Query::select ('t', array (), array ( +'type' => array ('admin', Query::PARAM_STR, 'NOT'), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("type != ", $sql); +} + +public function testWhereGreaterThan () +{ +$query = Query::select ('t', array (), array ( +'age' => array (18, Query::PARAM_NUMBER, '>'), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("age > ", $sql); +$this->assertStringContainsString ("18", $sql); +} + +public function testWhereLessThan () +{ +$query = Query::select ('t', array (), array ( +'age' => array (65, Query::PARAM_NUMBER, '<'), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("age < 65", $sql); +} + +public function testWhereGreaterOrEqual () +{ +$query = Query::select ('t', array (), array ( +'score' => array (100, Query::PARAM_NUMBER, '>='), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("score >= 100", $sql); +} + +public function testWhereLessOrEqual () +{ +$query = Query::select ('t', array (), array ( +'score' => array (50, Query::PARAM_NUMBER, '<='), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("score <= 50", $sql); +} + +public function testWhereNotEqualsOperator () +{ +$query = Query::select ('t', array (), array ( +'status' => array (0, Query::PARAM_NUMBER, '!='), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("status != 0", $sql); +} + +public function testWhereInOperator () +{ +$query = Query::select ('t', array (), array ( +'id' => array (array (1, 2, 3), Query::PARAM_NUMBER, 'IN'), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("id IN ", $sql); +$this->assertStringContainsString ("(1,2,3)", $sql); +} + +public function testWhereArrayImplicitIn () +{ +$query = Query::select ('t', array (), array ( +'id' => array (array (5, 10, 15), Query::PARAM_NUMBER), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("id IN ", $sql); +} + +// --------------------------------------------------------------- +// SELECT builder +// --------------------------------------------------------------- + +public function testSelectAllColumns () +{ +$query = Query::select ('users', array (), array ('active' => array (1, Query::PARAM_NUMBER))); +$sql = $query->getParsedQuery (); + +$this->assertStringStartsWith ("SELECT *", $sql); +$this->assertStringContainsString ("FROM `users`", $sql); +$this->assertStringContainsString ("WHERE active = 1", $sql); +} + +public function testSelectSpecificColumns () +{ +$query = Query::select ('users', array ('id', 'email'), array ()); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("SELECT id, email", $sql); +$this->assertStringContainsString ("FROM `users`", $sql); +} + +public function testSelectNoWhere () +{ +$query = Query::select ('users', array ('id')); +$sql = $query->getParsedQuery (); + +$this->assertStringNotContainsString ("WHERE", $sql); +} + +public function testSelectWithOrder () +{ +$query = Query::select ('users', array ('id'), array (), array ('name ASC', 'created DESC')); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("ORDER BY name ASC, created DESC", $sql); +} + +public function testSelectWithLimit () +{ +$query = Query::select ('users', array ('id'), array (), array (), '10, 20'); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("LIMIT 10, 20", $sql); +} + +public function testSelectWithOrderAndLimit () +{ +$query = Query::select ('users', array ('id'), array (), array ('id ASC'), '0, 5'); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("ORDER BY id ASC", $sql); +$this->assertStringContainsString ("LIMIT 0, 5", $sql); +} + +// --------------------------------------------------------------- +// INSERT builder +// --------------------------------------------------------------- + +public function testInsertBasic () +{ +$query = Query::insert ('users', array ( +'name' => 'Alice', +'age' => array (30, Query::PARAM_NUMBER), +)); +$sql = $query->getParsedQuery (); + +$this->assertStringStartsWith ("INSERT INTO `users`", $sql); +$this->assertStringContainsString ("name = 'Alice'", $sql); +$this->assertStringContainsString ("age = 30", $sql); +} + +public function testInsertEscapesSingleQuote () +{ +$query = Query::insert ('users', array ('name' => "O'Brien")); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("\\'", $sql); +} + +public function testInsertWithInjectionPayload () +{ +$query = Query::insert ('log', array ('msg' => "'; DROP TABLE users;--")); +$sql = $query->getParsedQuery (); + +// Single quote is escaped — the payload cannot break out of the string context +$this->assertStringContainsString ("\\'", $sql); +$this->assertStringContainsString ("msg = '", $sql); +} + +public function testInsertWithNullValue () +{ +$query = Query::insert ('users', array ('id' => 1, 'bio' => null)); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("bio = NULL", $sql); +} + +// --------------------------------------------------------------- +// REPLACE builder +// --------------------------------------------------------------- + +public function testReplaceBasic () +{ +$query = Query::replace ('users', array ( +'id' => array (1, Query::PARAM_NUMBER), +'name' => 'Bob', +)); +$sql = $query->getParsedQuery (); + +$this->assertStringStartsWith ("REPLACE INTO `users`", $sql); +$this->assertStringContainsString ("id = 1", $sql); +$this->assertStringContainsString ("name = 'Bob'", $sql); +} + +public function testReplaceWithInjectionPayload () +{ +$query = Query::replace ('users', array ('name' => "'; DROP TABLE users;--")); +$sql = $query->getParsedQuery (); + +// Single quote is escaped +$this->assertStringContainsString ("\\'", $sql); +} + +// --------------------------------------------------------------- +// UPDATE builder +// --------------------------------------------------------------- + +public function testUpdateBasic () +{ +$query = Query::update ( +'users', +array ('name' => 'Alice'), +array ('id' => array (1, Query::PARAM_NUMBER)) +); +$sql = $query->getParsedQuery (); + +$this->assertStringStartsWith ("UPDATE `users`", $sql); +$this->assertStringContainsString ("SET name = 'Alice'", $sql); +$this->assertStringContainsString ("WHERE id = 1", $sql); +} + +public function testUpdateWithInjectionInSet () +{ +$query = Query::update ( +'users', +array ('bio' => "'; DROP TABLE secrets;--"), +array ('id' => array (42, Query::PARAM_NUMBER)) +); +$sql = $query->getParsedQuery (); + +// Single quote escaped in the SET clause +$this->assertStringContainsString ("\\'", $sql); +} + +public function testUpdateWithInjectionInWhere () +{ +$query = Query::update ( +'users', +array ('bio' => 'safe value'), +array ('name' => "' OR '1'='1") +); +$sql = $query->getParsedQuery (); + +// Single quote in WHERE value must be escaped +$this->assertStringContainsString ("\\'", $sql); +} + +// --------------------------------------------------------------- +// DELETE builder +// --------------------------------------------------------------- + +public function testDeleteBasic () +{ +$query = Query::delete ('users', array ('id' => array (5, Query::PARAM_NUMBER))); +$sql = $query->getParsedQuery (); + +$this->assertStringStartsWith ("DELETE FROM `users`", $sql); +$this->assertStringContainsString ("WHERE id = 5", $sql); +} + +public function testDeleteWithInjectionInWhere () +{ +$query = Query::delete ('users', array ('name' => "' OR '1'='1")); +$sql = $query->getParsedQuery (); + +// Single quote escaped +$this->assertStringContainsString ("\\'", $sql); +} + +public function testDeleteNoWhere () +{ +$query = Query::delete ('t', array ()); +$sql = $query->getParsedQuery (); + +$this->assertStringStartsWith ("DELETE FROM `t`", $sql); +$this->assertStringNotContainsString ("WHERE", $sql); +} + +// --------------------------------------------------------------- +// Named parameters injection +// --------------------------------------------------------------- + +public function testNamedParamInjection () +{ +$query = new Query ("SELECT * FROM `users` WHERE name = :name AND role = :role"); +$query->bindValue ('name', "' OR '1'='1"); +$query->bindValue ('role', 'admin'); +$sql = $query->getParsedQuery (); + +// Single quote in named param must be escaped +$this->assertStringContainsString ("\\'", $sql); +$this->assertStringContainsString ("role = 'admin'", $sql); +} + +public function testNamedParamDoesNotReplaceItselfInValue () +{ +$query = new Query ("INSERT INTO `t` SET a = :a, b = :b"); +$query->bindValue ('a', 'value with :b placeholder'); +$query->bindValue ('b', 'real b'); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("a = 'value with :b placeholder'", $sql); +$this->assertStringContainsString ("b = 'real b'", $sql); +} + +// --------------------------------------------------------------- +// Positional parameters with injection +// --------------------------------------------------------------- + +public function testPositionalParamsWithInjection () +{ +$query = new Query ("SELECT * FROM `t` WHERE a = ? AND b = ?"); +$query->bindValue (1, "' OR 1=1--"); +$query->bindValue (2, "'; DROP TABLE t;--"); +$sql = $query->getParsedQuery (); + +// Both single quotes must be escaped +$this->assertStringContainsString ("\\'", $sql); +} + +public function testQuestionMarkInValueDoesNotBreakParsing () +{ +$query = new Query ("INSERT INTO `t` SET msg = ?, other = ?"); +$query->bindValue (1, "Is this a question?"); +$query->bindValue (2, "yes"); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("msg = 'Is this a question?'", $sql); +$this->assertStringContainsString ("other = 'yes'", $sql); +} + +// --------------------------------------------------------------- +// bindValue chaining +// --------------------------------------------------------------- + +public function testBindValueChaining () +{ +$query = (new Query ("SELECT * FROM `t` WHERE a = ? AND b = ?")) +->bindValue (1, 'foo') +->bindValue (2, 'bar'); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ("a = 'foo'", $sql); +$this->assertStringContainsString ("b = 'bar'", $sql); +} + +// --------------------------------------------------------------- +// Table name escaping +// --------------------------------------------------------------- + +public function testTableNameIsBacktickEscaped () +{ +$query = Query::select ('my_table', array ('id')); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('`my_table`', $sql); +} + +public function testInsertTableNameIsBacktickEscaped () +{ +$query = Query::insert ('log_entries', array ('msg' => 'test')); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('`log_entries`', $sql); +} + +// --------------------------------------------------------------- +// Edge cases with special characters +// --------------------------------------------------------------- + +public function testCarriageReturnEscaped () +{ +$query = new Query ("INSERT INTO `t` SET data = ?"); +$query->bindValue (1, "line1\rline2", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('\\r', $sql); +} + +public function testSubstituteCharacterEscaped () +{ +$query = new Query ("INSERT INTO `t` SET data = ?"); +$query->bindValue (1, "data\x1amore", Query::PARAM_STR); +$sql = $query->getParsedQuery (); + +$this->assertStringContainsString ('\\Z', $sql); +} + +public function testMultipleInjectionVectorsInSingleQuery () +{ +$query = Query::insert ('audit_log', array ( +'user' => "' OR '1'='1", +'action' => "'; DROP TABLE audit_log;--", +'payload' => "' UNION SELECT password FROM users--", +)); +$sql = $query->getParsedQuery (); + +// All dangerous single quotes must be escaped +$this->assertGreaterThanOrEqual (3, substr_count ($sql, "\\'")); +} +} diff --git a/src/Neuron/Tests/DbQueryTest.php b/tests/DbQueryTest.php similarity index 82% rename from src/Neuron/Tests/DbQueryTest.php rename to tests/DbQueryTest.php index 6f68c94..c5e0a34 100644 --- a/src/Neuron/Tests/DbQueryTest.php +++ b/tests/DbQueryTest.php @@ -1,17 +1,23 @@ bindValue (1, gmmktime (0, 0, 0, 9, 16, 2013), Query::PARAM_DATE); @@ -35,6 +41,9 @@ public function testQueryBuilder () $this->assertEquals ($expected, $sql); } + /** + * @test + */ public function testQueryBuilderSelect () { $expected1 = "SELECT column1, column2, column3 FROM `table` WHERE m_id = 1 AND m_date = FROM_UNIXTIME(1379289600) AND m_string = 'Value 1'"; @@ -58,6 +67,9 @@ public function testQueryBuilderSelect () $this->assertEquals ($expected2, $selectsql); } + /** + * @test + */ public function testQueryBuilderInsert () { // Insert query with only strings @@ -95,6 +107,9 @@ public function testQueryBuilderInsert () $this->assertEquals ($expected, $sql); } + /** + * @test + */ public function testQueryBuilderUpdate () { // And do the same with update @@ -140,6 +155,9 @@ public function testQueryBuilderUpdate () $this->assertEquals ($expected, $sql); } + /** + * @test + */ public function testQueryOrder () { $expected1 = "SELECT column1, column2, column3 FROM `table` WHERE m_id = 1 AND m_date = FROM_UNIXTIME(1379289600) AND m_string = 'Value 1' ORDER BY m_id ASC"; @@ -159,6 +177,9 @@ public function testQueryOrder () $this->assertEquals ($expected1, $selectsql); } + /** + * @test + */ public function testQueryLimit () { $expected1 = "SELECT column1, column2, column3 FROM `table` WHERE m_id = 1 AND m_date = FROM_UNIXTIME(1379289600) AND m_string = 'Value 1' LIMIT 0, 10"; @@ -178,6 +199,9 @@ public function testQueryLimit () $this->assertEquals ($expected1, $selectsql); } + /** + * @test + */ public function testQueryOrderAndLimit () { $expected1 = "SELECT column1, column2, column3 FROM `table` WHERE m_id = 1 AND m_date = FROM_UNIXTIME(1379289600) AND m_string = 'Value 1' ORDER BY m_id ASC LIMIT 0, 10"; @@ -198,6 +222,9 @@ public function testQueryOrderAndLimit () $this->assertEquals ($expected1, $selectsql); } + /** + * @test + */ public function testStupidQuestionmarkReplace () { $values = array @@ -210,11 +237,14 @@ public function testStupidQuestionmarkReplace () // Insert $query = Query::insert ('table', $values); - $expected = "INSERT INTO `table` SET m_id = '1', m_test = 'test string with a random ? in it.', m_next = 'another parameter'"; + $expected = "INSERT INTO `table` SET m_id = 1, m_test = 'test string with a random ? in it.', m_next = 'another parameter'"; $this->assertEquals ($expected, $query->getParsedQuery ()); } + /** + * @test + */ public function testStupidNamedParameterReplace () { // Insert @@ -224,11 +254,14 @@ public function testStupidNamedParameterReplace () $query->bindValue ('m_next', 'another parameter'); $query->bindValue ('m_id', 1); - $expected = "INSERT INTO `table` SET m_id = '1', m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; + $expected = "INSERT INTO `table` SET m_id = 1, m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; $this->assertEquals ($expected, $query->getParsedQuery ()); } + /** + * @test + */ public function testStupidMixedNameAndQuestionmarks () { // Insert @@ -238,7 +271,7 @@ public function testStupidMixedNameAndQuestionmarks () $query->bindValue (2, 'another parameter'); $query->bindValue ('m_id', 1); - $expected = "INSERT INTO `table` SET m_id = '1', m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; + $expected = "INSERT INTO `table` SET m_id = 1, m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; $this->assertEquals ($expected, $query->getParsedQuery ()); } @@ -252,11 +285,14 @@ public function testStupidMixedNameAndQuestionmarksReversed () $query->bindValue ('m_test', 'test string with a random :m_next parameter in it.'); $query->bindValue (2, 'another parameter'); - $expected = "INSERT INTO `table` SET m_id = '1', m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; + $expected = "INSERT INTO `table` SET m_id = 1, m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; $this->assertEquals ($expected, $query->getParsedQuery ()); } + /** + * @test + */ public function testStupidZeroBasedQuestionmarks () { // Insert @@ -266,8 +302,47 @@ public function testStupidZeroBasedQuestionmarks () $query->bindValue (1, 'test string with a random :m_next parameter in it.'); $query->bindValue (2, 'another parameter'); - $expected = "INSERT INTO `table` SET m_id = '1', m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; + $expected = "INSERT INTO `table` SET m_id = 1, m_test = 'test string with a random :m_next parameter in it.', m_next = 'another parameter'"; $this->assertEquals ($expected, $query->getParsedQuery ()); } + + /** + * @test + */ + public function testNullValues() + { + $query = Query::select( + 'tableName', + array('id'), + array( + 'id' => 1, + 'deleted_at' => null + ) + )->getParsedQuery(); + + $this->assertEquals( + 'SELECT id FROM `tableName` WHERE id = 1 AND deleted_at IS NULL', + $query + ); + } + + /** + * @test + */ + public function testNullInsert() + { + $query = Query::insert( + 'tableName', + array( + 'id' => 1, + 'name' => null + ) + )->getParsedQuery(); + + $this->assertEquals( + 'INSERT INTO `tableName` SET id = 1, name = NULL', + $query + ); + } } \ No newline at end of file diff --git a/tests/ErrorCollectionTest.php b/tests/ErrorCollectionTest.php new file mode 100644 index 0000000..5b8786e --- /dev/null +++ b/tests/ErrorCollectionTest.php @@ -0,0 +1,55 @@ +addError ('Something went wrong'); + + $this->assertCount (1, $collection); + $this->assertInstanceOf (\Neuron\Models\Error::class, $error); + } + + public function testGetData () + { + $collection = new ErrorCollection (); + $collection->addError ('Error %s', ['one']); + $collection->addError ('Error %s', ['two']); + + $data = $collection->getData (); + $this->assertCount (2, $data); + $this->assertEquals ('Error one', $data[0]); + $this->assertEquals ('Error two', $data[1]); + } + + public function testGetDetailedData () + { + $collection = new ErrorCollection (); + $error = $collection->addError ('Error %s in %s', ['field', 'form']); + $error->setSubject ('test_subject'); + $error->setCode ('ERR001'); + + $detailed = $collection->getDetailedData (); + $this->assertCount (1, $detailed); + $this->assertEquals ('Error field in form', $detailed[0]['message']); + $this->assertEquals ('Error %s in %s', $detailed[0]['template']); + $this->assertEquals (['field', 'form'], $detailed[0]['arguments']); + $this->assertEquals ('test_subject', $detailed[0]['subject']); + $this->assertEquals ('ERR001', $detailed[0]['code']); + } + + public function testAddErrorWithNoArguments () + { + $collection = new ErrorCollection (); + $collection->addError ('Simple error message'); + + $data = $collection->getData (); + $this->assertEquals ('Simple error message', $data[0]); + } +} diff --git a/src/Neuron/Tests/FilterTest.php b/tests/FilterTest.php similarity index 99% rename from src/Neuron/Tests/FilterTest.php rename to tests/FilterTest.php index e79f8f6..6fd1993 100644 --- a/src/Neuron/Tests/FilterTest.php +++ b/tests/FilterTest.php @@ -10,13 +10,13 @@ use Neuron\Filter\Context; use Neuron\Filter\Field; -use PHPUnit_Framework_TestCase; +use PHPUnit\Framework\TestCase; use Neuron\Filter\Parser; use Neuron\Filter\Scanner; class FilterTest - extends PHPUnit_Framework_TestCase + extends TestCase { public function testFilter () diff --git a/tests/SimpleCryptTest.php b/tests/SimpleCryptTest.php new file mode 100644 index 0000000..34851bc --- /dev/null +++ b/tests/SimpleCryptTest.php @@ -0,0 +1,81 @@ +encrypt ($original); + $this->assertNotEquals ($original, $encrypted); + + $decrypted = $crypt->decrypt ($encrypted); + $this->assertEquals ($original, $decrypted); + } + + public function testDifferentPasswordsFail () + { + $crypt1 = new SimpleCrypt ('password1'); + $crypt2 = new SimpleCrypt ('password2'); + + $encrypted = $crypt1->encrypt ('secret'); + $decrypted = $crypt2->decrypt ($encrypted); + + $this->assertNotEquals ('secret', $decrypted); + } + + public function testEncryptProducesDifferentOutput () + { + $crypt = new SimpleCrypt ('password'); + + $encrypted1 = $crypt->encrypt ('same text'); + $encrypted2 = $crypt->encrypt ('same text'); + + // Due to random salt, encrypted values should differ + $this->assertNotEquals ($encrypted1, $encrypted2); + } + + public function testEncryptDecryptEmptyString () + { + $crypt = new SimpleCrypt ('password'); + $encrypted = $crypt->encrypt (''); + $decrypted = $crypt->decrypt ($encrypted); + $this->assertEquals ('', $decrypted); + } + + public function testEncryptDecryptSpecialCharacters () + { + $crypt = new SimpleCrypt ('password'); + $original = "Special chars: !@#\$%^&*()_+-=[]{}|;':\",./<>?"; + + $encrypted = $crypt->encrypt ($original); + $decrypted = $crypt->decrypt ($encrypted); + $this->assertEquals ($original, $decrypted); + } + + public function testEncryptDecryptUTF8 () + { + $crypt = new SimpleCrypt ('password'); + $original = 'Héllo Wörld 日本語'; + + $encrypted = $crypt->encrypt ($original); + $decrypted = $crypt->decrypt ($encrypted); + $this->assertEquals ($original, $decrypted); + } + + public function testEncryptDecryptWithSaltMarkerInContent () + { + $crypt = new SimpleCrypt ('password'); + $original = 'Text with |||CWSALT inside it'; + + $encrypted = $crypt->encrypt ($original); + $decrypted = $crypt->decrypt ($encrypted); + $this->assertEquals ($original, $decrypted); + } +} diff --git a/tests/TestDatabase.php b/tests/TestDatabase.php new file mode 100644 index 0000000..12d1b70 --- /dev/null +++ b/tests/TestDatabase.php @@ -0,0 +1,49 @@ + "\\\\", + "\x00" => "\\0", + "\n" => "\\n", + "\r" => "\\r", + "'" => "\\'", + '"' => '\\"', + "\x1a" => "\\Z", + ]); + } + + public function query ($sSQL): int + { + return 0; + } + + public function multiQuery ($sSQL): int + { + return 0; + } + + public function fromUnixtime ($timestamp): string + { + return date ('Y-m-d H:i:s', $timestamp); + } + + public function toUnixtime ($date): int + { + return strtotime ($date); + } +} diff --git a/tests/TokenGeneratorTest.php b/tests/TokenGeneratorTest.php new file mode 100644 index 0000000..a2f1693 --- /dev/null +++ b/tests/TokenGeneratorTest.php @@ -0,0 +1,19 @@ +assertTrue (strlen ($token) === 50); + } + } +} \ No newline at end of file diff --git a/tests/ToolsTest.php b/tests/ToolsTest.php new file mode 100644 index 0000000..6c92292 --- /dev/null +++ b/tests/ToolsTest.php @@ -0,0 +1,588 @@ +assertTrue (Tools::checkInput ('thijs@catlab.be', 'email')); + $this->assertTrue (Tools::checkInput ('thijs.vanderschaeghe@catlab.be', 'email')); + + // Invalid email address + $this->assertFalse (Tools::checkInput (0, 'email')); + $this->assertFalse (Tools::checkInput (null, 'email')); + $this->assertFalse (Tools::checkInput (false, 'email')); + $this->assertFalse (Tools::checkInput ('thijs', 'email')); + $this->assertFalse (Tools::checkInput ('@catlab.be', 'email')); + $this->assertFalse (Tools::checkInput ('thijs@home@catlab.be', 'email')); + } + + public function testURLInputCheck () + { + //$this->assertTrue (Tools::checkInput ('huffingtonpost.com/2014/06/13/iraq-defend-country_n_5491357.html?1402661760', 'url')); + + $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.catlab.eu', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.catlab.eu?foo=bar&bla=bam', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/?foo=bar&bla=bam', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/index.html?foo=bar&bla=bam', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.catlab.eu/index.php?foo=bar&bla=bam', 'url')); + + $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/', 'url')); + $this->assertTrue (Tools::checkInput ('https://www.catlab.eu', 'url')); + $this->assertTrue (Tools::checkInput ('https://www.catlab.eu?foo=bar&bla=bam', 'url')); + $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/?foo=bar&bla=bam', 'url')); + $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/index.html?foo=bar&bla=bam', 'url')); + $this->assertTrue (Tools::checkInput ('https://www.catlab.eu/index.php?foo=bar&bla=bam', 'url')); + $this->assertTrue (Tools::checkInput ('http://socialmouths.com/blog/2014/01/24/google-plus-features/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+Socialmouths+%28SocialMouths%29', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.business2community.com/social-media/social-media-strategy-wont-work-without-one-thing-0911103#!YluUO', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.latimes.com/world/middleeast/la-fg-obama-iraq-20140613-story.html#page=1', 'url')); + $this->assertTrue (Tools::checkInput ('http://www.huffingtonpost.com/2014/06/13/iraq-defend-country_n_5491357.html?1402661760', 'url')); + $this->assertTrue (Tools::checkInput ('ww.link.be', 'url')); + + $this->assertTrue (Tools::checkInput ('www.huffingtonpost.com/2014/06/13/iraq-defend-country_n_5491357.html?1402661760', 'url')); + + $this->assertFalse (Tools::checkInput ('this is not an url.', 'url')); + $this->assertFalse (Tools::checkInput ('thisisalsonotanurl.', 'url')); + $this->assertFalse (Tools::checkInput ('.neitheristhis', 'url')); + $this->assertFalse (Tools::checkInput ('.or this', 'url')); + + //$this->assertFalse (Tools::checkInput ('iwouldliketobeanurl.but im not', 'url')); + + $this->assertFalse (Tools::checkInput ('test', 'url')); +// $this->assertFalse (Tools::checkInput ('w.test', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('w.test.com', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('ftp://user:password@domain.com/path/', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('https://www.test.subdomain.domain.xyz/', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('domain.test/#anchor', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('domain.co/?query=123', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('mailto://user@unkwn.com', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('http://www.domain.co/path/to/index.ext', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('http://www.domain.co\path\to\stuff.txt', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('http://www.domain.co\path@to#stuff$txt', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('www.test.com/file[/]index.html', 'url')); // @TODO +// $this->assertFalse (Tools::checkInput ('www.test.com/file{/}index.html', 'url')); // @TODO + $this->assertFalse (Tools::checkInput ('www."test".com', 'url')); + } + + public function testNumberInput () + { + $this->assertTrue (Tools::checkInput (5, 'number')); + $this->assertTrue (Tools::checkInput (5.0, 'number')); + $this->assertTrue (Tools::checkInput ('5.0', 'number')); + + $this->assertFalse (Tools::checkInput ('five', 'number')); + $this->assertFalse (Tools::checkInput ('23,5', 'number')); + $this->assertFalse (Tools::checkInput ('foobaaaar', 'number')); + } + + public function testIntInput () + { + $this->assertTrue (Tools::checkInput (5, 'int')); + $this->assertTrue (Tools::checkInput (5.0, 'int')); + $this->assertTrue (Tools::checkInput ('5', 'int')); + $this->assertTrue (Tools::checkInput ('5.0', 'int')); + + $this->assertFalse (Tools::checkInput (5.1, 'int')); + $this->assertFalse (Tools::checkInput ('5.1', 'int')); + $this->assertFalse (Tools::checkInput ('foobar', 'int')); + $this->assertFalse (Tools::checkInput ('23,5', 'int')); + + } + + public function testDateInput () { + $this->assertTrue (Tools::checkInput ('2015-06-01T10:00', 'datetime')); + $this->assertFalse (Tools::checkInput ('06-01-2015T10:00', 'datetime')); + } + + // --------------------------------------------------------------- + // Date validation tests (the bug fix) + // --------------------------------------------------------------- + + public function testDateCheckInputValidDates () + { + $this->assertTrue (Tools::checkInput ('2015-06-01', 'date')); + $this->assertTrue (Tools::checkInput ('2000-01-01', 'date')); + $this->assertTrue (Tools::checkInput ('1999-12-31', 'date')); + $this->assertTrue (Tools::checkInput ('2024-02-29', 'date')); // leap year + } + + public function testDateCheckInputInvalidNonIntegerParts () + { + // The original bug: "a-b-c" would pass + $this->assertFalse (Tools::checkInput ('a-b-c', 'date')); + $this->assertFalse (Tools::checkInput ('foo-bar-baz', 'date')); + $this->assertFalse (Tools::checkInput ('20xx-01-01', 'date')); + $this->assertFalse (Tools::checkInput ('2015-ab-01', 'date')); + $this->assertFalse (Tools::checkInput ('2015-01-cd', 'date')); + } + + public function testDateCheckInputInvalidDateValues () + { + $this->assertFalse (Tools::checkInput ('2015-13-01', 'date')); // month 13 + $this->assertFalse (Tools::checkInput ('2015-00-01', 'date')); // month 0 + $this->assertFalse (Tools::checkInput ('2015-02-30', 'date')); // Feb 30 + $this->assertFalse (Tools::checkInput ('2023-02-29', 'date')); // non-leap year + $this->assertFalse (Tools::checkInput ('2015-06-32', 'date')); // day 32 + $this->assertFalse (Tools::checkInput ('0000-01-01', 'date')); // year 0 + } + + public function testDateCheckInputInvalidFormats () + { + $this->assertFalse (Tools::checkInput ('', 'date')); + $this->assertFalse (Tools::checkInput ('2015', 'date')); + $this->assertFalse (Tools::checkInput ('2015-06', 'date')); + $this->assertFalse (Tools::checkInput ('2015/06/01', 'date')); + $this->assertFalse (Tools::checkInput ('01-06-2015', 'date')); // wrong order but valid checkdate would pass + $this->assertFalse (Tools::checkInput ('2015-06-01-extra', 'date')); + } + + public function testDateGetInputValidDate () + { + $dat = array ('date' => '2015-06-01'); + $result = Tools::getInput ($dat, 'date', 'date'); + $this->assertIsInt ($result); + $this->assertEquals ('2015-06-01', date ('Y-m-d', $result)); + } + + public function testDateGetInputInvalidDate () + { + $dat = array ('date' => 'a-b-c'); + $result = Tools::getInput ($dat, 'date', 'date'); + $this->assertNull ($result); + } + + public function testDateGetInputMissing () + { + $dat = array (); + $result = Tools::getInput ($dat, 'date', 'date'); + $this->assertNull ($result); + } + + public function testDateGetInputDefault () + { + $dat = array ('date' => 'invalid'); + $result = Tools::getInput ($dat, 'date', 'date', 'default_value'); + $this->assertEquals ('default_value', $result); + } + + // --------------------------------------------------------------- + // Datetime validation tests + // --------------------------------------------------------------- + + public function testDatetimeCheckInputValid () + { + $this->assertTrue (Tools::checkInput ('2015-06-01T10:00', 'datetime')); + $this->assertTrue (Tools::checkInput ('2024-12-31T23:59', 'datetime')); + } + + public function testDatetimeCheckInputInvalid () + { + $this->assertFalse (Tools::checkInput ('06-01-2015T10:00', 'datetime')); + $this->assertFalse (Tools::checkInput ('not-a-datetime', 'datetime')); + $this->assertFalse (Tools::checkInput ('2015-06-01 10:00', 'datetime')); + $this->assertFalse (Tools::checkInput ('', 'datetime')); + } + + public function testDatetimeGetInputValid () + { + $dat = array ('dt' => '2015-06-01T10:00'); + $result = Tools::getInput ($dat, 'dt', 'datetime'); + $this->assertInstanceOf (\DateTime::class, $result); + } + + // --------------------------------------------------------------- + // Text and varchar type tests + // --------------------------------------------------------------- + + public function testTextCheckInput () + { + $this->assertTrue (Tools::checkInput ('anything', 'text')); + $this->assertTrue (Tools::checkInput ('', 'text')); + $this->assertTrue (Tools::checkInput ('', 'text')); + } + + public function testVarcharCheckInput () + { + $this->assertTrue (Tools::checkInput ('valid text', 'varchar')); + $this->assertTrue (Tools::checkInput ('valid text', 'string')); + $this->assertTrue (Tools::checkInput ('valid html', 'html')); + } + + public function testNameCheckInput () + { + $this->assertTrue (Tools::checkInput ('John Doe', 'name')); + $this->assertFalse (Tools::checkInput ('John', 'name')); + $this->assertFalse (Tools::checkInput ('', 'name')); + } + + // --------------------------------------------------------------- + // Bool type tests + // --------------------------------------------------------------- + + public function testBoolCheckInput () + { + $this->assertTrue (Tools::checkInput (1, 'bool')); + $this->assertTrue (Tools::checkInput ('true', 'bool')); + $this->assertFalse (Tools::checkInput (0, 'bool')); + $this->assertFalse (Tools::checkInput ('false', 'bool')); + $this->assertFalse (Tools::checkInput ('', 'bool')); + } + + public function testBoolGetInput () + { + // When checkInput passes (value is 1 or 'true'), getInput returns strip_tags(value) + $dat = array ('flag' => 1); + $this->assertEquals ('1', Tools::getInput ($dat, 'flag', 'bool')); + + $dat = array ('flag' => 'true'); + $this->assertEquals ('true', Tools::getInput ($dat, 'flag', 'bool')); + + // When bool validation fails, getInput returns false (special case) + $dat = array ('flag' => 0); + $this->assertFalse (Tools::getInput ($dat, 'flag', 'bool')); + + $dat = array ('flag' => 'no'); + $this->assertFalse (Tools::getInput ($dat, 'flag', 'bool')); + } + + // --------------------------------------------------------------- + // Password type tests + // --------------------------------------------------------------- + + public function testPasswordCheckInput () + { + $this->assertTrue (Tools::checkInput ('password123', 'password')); + $this->assertTrue (Tools::checkInput ('12345678', 'password')); + $this->assertFalse (Tools::checkInput ('short', 'password')); + $this->assertFalse (Tools::checkInput ('1234567', 'password')); // 7 chars + $this->assertFalse (Tools::checkInput (str_repeat ('a', 257), 'password')); // too long + $this->assertTrue (Tools::checkInput (str_repeat ('a', 256), 'password')); // max allowed + } + + // --------------------------------------------------------------- + // Username type tests + // --------------------------------------------------------------- + + public function testUsernameCheckInput () + { + $this->assertTrue (Tools::checkInput ('john_doe', 'username')); + $this->assertTrue (Tools::checkInput ('User123', 'username')); + $this->assertTrue (Tools::checkInput ('abc', 'username')); // min 3 chars + $this->assertFalse (Tools::checkInput ('ab', 'username')); // too short + $this->assertFalse (Tools::checkInput ('', 'username')); + $this->assertFalse (Tools::checkInput ('user name', 'username')); // space + $this->assertFalse (Tools::checkInput ('user@name', 'username')); // special chars + $this->assertFalse (Tools::checkInput (str_repeat ('a', 21), 'username')); // too long + } + + // --------------------------------------------------------------- + // MD5 type tests + // --------------------------------------------------------------- + + public function testMd5CheckInput () + { + $this->assertTrue (Tools::checkInput (md5 ('test'), 'md5')); + $this->assertTrue (Tools::checkInput ('d41d8cd98f00b204e9800998ecf8427e', 'md5')); + $this->assertFalse (Tools::checkInput ('tooshort', 'md5')); + $this->assertFalse (Tools::checkInput ('', 'md5')); + $this->assertFalse (Tools::checkInput (str_repeat ('a', 33), 'md5')); + } + + // --------------------------------------------------------------- + // Base64 type tests + // --------------------------------------------------------------- + + public function testBase64CheckInput () + { + $this->assertTrue (Tools::checkInput (base64_encode ('test'), 'base64')); + $this->assertTrue (Tools::checkInput (base64_encode ('hello world'), 'base64')); + $this->assertFalse (Tools::checkInput ('not valid base64!!!', 'base64')); + } + + public function testBase64GetInput () + { + $dat = array ('data' => base64_encode ('hello world')); + $result = Tools::getInput ($dat, 'data', 'base64'); + $this->assertEquals ('hello world', $result); + } + + // --------------------------------------------------------------- + // Raw type tests + // --------------------------------------------------------------- + + public function testRawCheckInput () + { + $this->assertTrue (Tools::checkInput ('anything', 'raw')); + $this->assertTrue (Tools::checkInput ('', 'raw')); + } + + public function testRawGetInput () + { + $dat = array ('data' => ''); + $result = Tools::getInput ($dat, 'data', 'raw'); + $this->assertEquals ('', $result); + } + + public function testHtmlGetInput () + { + $dat = array ('data' => 'bold'); + $result = Tools::getInput ($dat, 'data', 'html'); + $this->assertEquals ('bold', $result); + } + + // --------------------------------------------------------------- + // Unknown type tests + // --------------------------------------------------------------- + + public function testUnknownTypeReturnsFalse () + { + $this->assertFalse (Tools::checkInput ('value', 'nonexistent_type')); + } + + // --------------------------------------------------------------- + // getInput default behavior tests + // --------------------------------------------------------------- + + public function testGetInputMissingKey () + { + $dat = array (); + $this->assertNull (Tools::getInput ($dat, 'missing', 'text')); + } + + public function testGetInputMissingKeyWithDefault () + { + $dat = array (); + $this->assertEquals ('fallback', Tools::getInput ($dat, 'missing', 'text', 'fallback')); + } + + public function testGetInputStripsTagsByDefault () + { + $dat = array ('name' => 'John Doe'); + $result = Tools::getInput ($dat, 'name', 'varchar'); + $this->assertEquals ('John Doe', $result); + } + + // --------------------------------------------------------------- + // SQL injection patterns via checkInput + // --------------------------------------------------------------- + + public function testSqlInjectionInEmailField () + { + $this->assertFalse (Tools::checkInput ("' OR '1'='1", 'email')); + $this->assertFalse (Tools::checkInput ("admin@example.com' OR 1=1--", 'email')); + $this->assertFalse (Tools::checkInput ("admin@example.com'; DROP TABLE users;--", 'email')); + $this->assertFalse (Tools::checkInput ("' UNION SELECT * FROM users--", 'email')); + } + + public function testSqlInjectionInUsernameField () + { + $this->assertFalse (Tools::checkInput ("' OR '1'='1", 'username')); + $this->assertFalse (Tools::checkInput ("admin'; DROP TABLE users;--", 'username')); + $this->assertFalse (Tools::checkInput ("admin' UNION SELECT * FROM users--", 'username')); + $this->assertFalse (Tools::checkInput ("1; DROP TABLE users", 'username')); + } + + public function testSqlInjectionInDateField () + { + $this->assertFalse (Tools::checkInput ("'; DROP TABLE users;--", 'date')); + $this->assertFalse (Tools::checkInput ("2015-01-01' OR '1'='1", 'date')); + $this->assertFalse (Tools::checkInput ("2015-01-01; DROP TABLE users;--", 'date')); + $this->assertFalse (Tools::checkInput ("1 OR 1=1", 'date')); + $this->assertFalse (Tools::checkInput ("' UNION SELECT * FROM users--", 'date')); + } + + public function testSqlInjectionInDatetimeField () + { + $this->assertFalse (Tools::checkInput ("'; DROP TABLE users;--", 'datetime')); + $this->assertFalse (Tools::checkInput ("not-a-datetime", 'datetime')); + } + + public function testSqlInjectionInIntField () + { + $this->assertFalse (Tools::checkInput ("1; DROP TABLE users", 'int')); + $this->assertFalse (Tools::checkInput ("1 OR 1=1", 'int')); + $this->assertFalse (Tools::checkInput ("' OR '1'='1", 'int')); + $this->assertFalse (Tools::checkInput ("1 UNION SELECT * FROM users", 'int')); + } + + public function testSqlInjectionInNumberField () + { + $this->assertFalse (Tools::checkInput ("1; DROP TABLE users", 'number')); + $this->assertFalse (Tools::checkInput ("1 OR 1=1", 'number')); + $this->assertFalse (Tools::checkInput ("' OR '1'='1", 'number')); + } + + public function testSqlInjectionInMd5Field () + { + $this->assertFalse (Tools::checkInput ("' OR '1'='1' --", 'md5')); + $this->assertFalse (Tools::checkInput ("'; DROP TABLE users;--", 'md5')); + } + + public function testSqlInjectionInPasswordField () + { + // These pass checkInput because password only checks length/UTF-8 + // But they should still be properly escaped before use in queries + $this->assertTrue (Tools::checkInput ("password' OR '1'='1", 'password')); + } + + public function testSqlInjectionInUrlField () + { + $this->assertFalse (Tools::checkInput ("'; DROP TABLE users;--", 'url')); + $this->assertFalse (Tools::checkInput ("' OR '1'='1", 'url')); + } + + // --------------------------------------------------------------- + // XSS injection patterns via getInput + // --------------------------------------------------------------- + + public function testXssStrippedByGetInputDefault () + { + $dat = array ('name' => ''); + $result = Tools::getInput ($dat, 'name', 'varchar'); + $this->assertStringNotContainsString (''); + $this->assertStringNotContainsString ('