{"id":13405344,"url":"https://github.com/OmarElgabry/miniPHP","last_synced_at":"2025-03-14T09:32:42.720Z","repository":{"id":43774492,"uuid":"38610620","full_name":"OmarElgabry/miniPHP","owner":"OmarElgabry","description":"A small, simple PHP MVC framework skeleton that encapsulates a lot of features surrounded with powerful security layers.","archived":false,"fork":false,"pushed_at":"2020-11-19T21:58:38.000Z","size":1648,"stargazers_count":162,"open_issues_count":11,"forks_count":53,"subscribers_count":22,"default_branch":"master","last_synced_at":"2024-07-31T19:45:45.814Z","etag":null,"topics":["framework","mvc","mvc-framework","php","php-framework","php-mvc-skeleton"],"latest_commit_sha":null,"homepage":"https://miniphp.ga/","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OmarElgabry.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-07-06T09:49:51.000Z","updated_at":"2024-07-11T20:27:22.000Z","dependencies_parsed_at":"2022-08-12T10:42:20.400Z","dependency_job_id":null,"html_url":"https://github.com/OmarElgabry/miniPHP","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OmarElgabry%2FminiPHP","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OmarElgabry%2FminiPHP/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OmarElgabry%2FminiPHP/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OmarElgabry%2FminiPHP/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OmarElgabry","download_url":"https://codeload.github.com/OmarElgabry/miniPHP/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243554305,"owners_count":20309908,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","mvc","mvc-framework","php","php-framework","php-mvc-skeleton"],"created_at":"2024-07-30T19:01:59.481Z","updated_at":"2025-03-14T09:32:42.234Z","avatar_url":"https://github.com/OmarElgabry.png","language":"PHP","funding_links":[],"categories":["PHP"],"sub_categories":[],"readme":"![miniPHP](https://raw.githubusercontent.com/OmarElGabry/miniPHP/master/public/img/backgrounds/background.png)\n\n# miniPHP\n[![Build Status](https://scrutinizer-ci.com/g/OmarElGabry/miniPHP/badges/build.png?b=master)](https://scrutinizer-ci.com/g/OmarElGabry/miniPHP/build-status/master)\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/OmarElGabry/miniPHP/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/OmarElGabry/miniPHP/?branch=master)\n[![Code Climate](https://codeclimate.com/github/OmarElGabry/miniPHP/badges/gpa.svg)](https://codeclimate.com/github/OmarElGabry/miniPHP)\n[![Dependency Status](https://www.versioneye.com/user/projects/55ae85dd3865620018000001/badge.svg?style=flat)](https://www.versioneye.com/user/projects/55ae85dd3865620018000001)\n\n[![Latest Stable Version](https://poser.pugx.org/omarelgabry/miniphp/v/stable)](https://packagist.org/packages/omarelgabry/miniphp)\n[![License](https://poser.pugx.org/omarelgabry/miniphp/license)](https://packagist.org/packages/omarelgabry/miniphp)\n\nA small, simple PHP MVC framework skeleton that encapsulates a lot of features surrounded with powerful security layers.\n\nminiPHP is a very simple application, useful for small projects, helps to understand the PHP MVC skeleton, know how to authenticate and authorize, encrypt data and apply security concepts, sanitization and validation, make ajax calls and more.\n\nIt's not a full framework, nor a very basic one but it's not complicated. You can easily install, understand, and use it in any of your projects.\n\nIt's indented to remove the complexity of the frameworks. Things like routing, authentication, authorization, manage user session and cookies, and so on are not something I've invented from the scratch, however, they are aggregation of concepts already implemented in other frameworks, but, built in a much simpler way, So, you can understand it, and take it further.\n\nIf you need to build bigger application, and take the advantage of most of the features available in frameworks, you can see [CakePHP](http://cakephp.org/), [Laravel](http://laravel.com/), [Symphony](http://symfony.com/).\n\nEither way, It's important to understand the PHP MVC skeleton, and know how to authenticate and authorize, learn about security issues and how can you defeat against, and how to build you own application using the framework.\n\n## Documentation\nFull Documentation can be also found [here](http://omarelgabry.github.io/miniPHP/) — created by GitHub automatic page generator.\n\n## Index\n+ [Demo](#live-demo)\n+ [Installation](#installation)\n+ [Routing](#routing)\n+ [Controller](#controller)\n+ [Components(Middlewares)](#components)\n\t+ [Authentication](#authentication)\n\t    - [Session](#session)\n\t    - [Cookies](#cookies)\n\t+ [Authorization](#authorization)\n\t+ [Security](#security)\n\t\t- [HTTP Methods](#http-method)\n\t\t- [Domain Validation](#referer)\n\t\t- [Form Tampering](#form-tampering)\n\t\t- [CSRF](#csrf)\n\t\t- [htaccess](#htaccess)\n\t+ [Turn on/off Components](#turn-on-off-components)\n+ [Views](#views)\n+ [Models](#models)\n+ [Login](#login)\n\t- [User Verification](#user-verification)\n\t- [Forgotten Password](#forgotten-password)\n\t- [Brute-Force attack](#brute-force)\n\t- [Captcha](#captcha)\n\t- [Block IP Addresses](#block-ip)\n+ [Database](#database)\n+ [Encryption](#encryption)\n+ [Validation](#validation)\n+ [Errors \u0026 Exceptions](#errors-exceptions)\n+ [Logger](#logger)\n+ [Email](#email)\n+ [Configurations](#configurations)\n+ [JavaScript \u0026 Ajax](#js)\n+ [Application(Demo)](#app-demo)\n\t+ [Intro](#intro-demo)\n\t+ [Installation](#installation-demo)\n\t+ [User Profile](#profile)\n\t+ [Files](#files)\n\t+ [News Feed \u0026 Posts \u0026 Comments](#newsfeed-posts-comments)\n\t+ [Admin](#admin)\n\t+ [Notifications](#notifications)\n\t+ [Report Bugs](#bugs)\n\t+ [Backups](#backups)\n+ [ToDo Application(Step By Step Implementation)](#todo)\n+ [Support](#support)\n+ [Contribute](#contribute)\n+ [Dependencies](#dependencies)\n+ [License](#license)\n\n## Demo \u003ca name=\"live-demo\"\u003e\u003c/a\u003e\nA live demo is available [here](https://miniphp.ga/). The live demo is for the demo application built on top of this framework in this [section](#app-demo). Thanks to [@Everterstraat](https://github.com/Everterstraat).\n\n\u003e Some features mighn't work in the demo.\n\n## Installation \u003ca name=\"installation\"\u003e\u003c/a\u003e\nInstall via [Composer](https://getcomposer.org/doc/00-intro.md)\n\n```\n\tcomposer install\n```\n\n## Routing \u003ca name=\"routing\"\u003e\u003c/a\u003e\n\nWhenever you make a request to the application, it wil be directed to _index.php_ inside public folder. \nSo, if you make a request: ```http://localhost/miniPHP/User/update/412 ```. This will be splitted and translated into \n\n+ Controller: User\n+ Action Method: update\n+ Arguemtns to action method: 412\n\nIn fact, htaccess splits everything comes after ```http://localhost/miniPHP ``` and adds it to the URL as querystring argument. So, this request will be converted to: ```http://localhost/miniPHP?url='User/update/412' ```.\n\nThen ```App``` Class, Inside ```splitUrl()```, will split the query string ```$_GET['url']``` into controller, action method, and any passed arguments to action method.\n\nIn ```App``` Class, Inside ```run()```, it will instantiate an object from controller class, and make a call to action method, passing any arguments if exist.\n\n## Controller \u003ca name=\"controller\"\u003e\u003c/a\u003e\n\nAfter the ```App``` Class intantiates controller object, It will call ```$this-\u003econtroller-\u003estartupProcess()``` method, which in turn will trigger 3 consecutive events/methods:\n\n1. ```initialize()```: Use it to load components\n2. ```beforeAction()```: Perform any logic actions before calling controller's action method\n3. ```triggerComponents()```: Trigger startup() method of loaded components\n\nThe constructor of ```Controller``` Class **shouldn't** be overridden, instead you can override the ```initialize()``` \u0026 ```beforeAction()``` methods in the extending classes.\n\nAfter the startup process of the constrcutor finishes it's job, Then, the requested action method will be called, and arguments will be passed(if any).\n\n## Components(Middlewares) \u003ca name=\"components\"\u003e\u003c/a\u003e\nComponents are the middlewares. They provide reusable logic to be used as part of the controller. Authentication, Authorization, Form Tampering, and Validate CSRF Tokens are implemented inside Components. \n\nIt's better to pull these pieces of logic out of controller class, and keep all various tasks and validations inside these Components.\n\nEvery component inherits from the base/super class called ```Component```. Each has a defined task. There are two components, one for called _Auth_ for Authentication and Authorization, and the other one called _Security_ for other Security Issues.\n\nThey are very simple to deal with, and they will be called inside controller constructor.\n\n### Authentication \u003ca name=\"authentication\"\u003e\u003c/a\u003e\nIs user has right credentials?\n\n#### Session\u003ca name=\"session\"\u003e\u003c/a\u003e\nThe AuthComponent takes care of user session.\n\n+ Prevent Session Concurrency\n\t- There can't be 2 users logged in with same user credentials.\n+ Defeat against Session Hijacking \u0026 Fixation\n\t- HTTP Only with session cookies\n\t- Whenever it's possible, It's Highly Recommended to use Secured connection(SSL).\n\t- Regenerate session periodically and after actions like login, forgot password, ...etc.\n\t- Validate user's IP Address and User agent(initially will be stored in session). Although they can be faked, It's better to keep them as part of validation methods.\n+ Session Expiration\n\t- Session will expire after certain duration(\u003e= 1 day)\n\t- Session cookie in browser is also configured to be expired after (\u003e= 1 week)\n+ Session accessible only through the HTTP protocol\n\t- This is important so sessions won't be accessible by JS.\n\n#### Cookies\u003ca name=\"cookies\"\u003e\u003c/a\u003e\n\n+ Remember Me Tokens\n\t- User can keep himself logged in using cookies\n\t- HTTP Only with cookies\n\t- Whenever it's possible, It's Highly Recommended to use Secured connection(SSL).\n\t- Cookies stored in browser are attached with tokens and Encrypted data\n\t- Cookies in browser are also configured to be expired after (\u003e= 2 weeks)\n\t\n### Authorization \u003ca name=\"authorization\"\u003e\u003c/a\u003e\nDo you have the right to access or to perform X action?. The _Auth_ Component takes care of authorization for each controller. Thus, each controller should implement ``` isAuthorized() ``` method. What you need to do is to return ``` boolean ``` value.\n\nSo, for example, in order to check if current user is admin or not, you would do something like this:\n```php\n    // AdminController\n\n    public function isAuthorized(){\n\n        $role = Session::getUserRole();\n        if(isset($role) \u0026\u0026 $role === \"admin\"){\n            return true;\n        }\n        return false;\n    }\n\n```\n\nIf you want to take it further and apply some permission rules, There is a powerful class called ``` Permission ``` responsible for defining permission rules. This class allows you to define \"Who is allowed to perform specific action method on current controller\".\n\nSo, for example, in order to allow admins to perform any action on notes, while normal users can only edit their notes:\n```php\n   // NotesController\n   \n   public function isAuthorized(){\n\n        $action = $this-\u003erequest-\u003eparam('action');\n        $role \t= Session::getUserRole();\n        $resource = \"notes\";\n\n\t\t// only for admins\n\t\t// they are allowed to perform all actions on $resource\n        Permission::allow('admin', $resource, ['*']);\n\n\t\t// for normal users, they can edit only if the current user is the owner\n\t\tPermission::allow('user', $resource, ['edit'], 'owner');\n\n        $noteId = $this-\u003erequest-\u003edata(\"note_id\");\n        $config = [\n            \"user_id\" =\u003e Session::getUserId(),\n            \"table\" =\u003e \"notes\",\n            \"id\" =\u003e $noteId\n        ];\n\n\t\t// providing the current user's role, $resource, action method, and some configuration data\n\t\t// Permission class will check based on rules defined above and return boolean value\n\t\treturn Permission::check($role, $resource, $action, $config);\n    }\n```\nNow, you can check authorization based on user's role, resource, and for each action method.\n\n### Security \u003ca name=\"security\"\u003e\u003c/a\u003e\nThe SecurityComponent takes care of various security tasks and validation. \n\n#### HTTP Method\u003ca name=\"http-method\"\u003e\u003c/a\u003e\n\nIt's important to restrict the request methods. As an example, if you have an action method that accepts form values, So, ONLY POST request will be accepted. The same idea for Ajax, GET, ..etc. You can do this inside ```beforeAction() ``` method. \n\n```php\n    // NotesController\n\n    public function beforeAction(){\n\n        parent::beforeAction();\n\n        $actions = ['create', 'delete'];\n\n        $this-\u003eSecurity-\u003erequireAjax($actions);\n        $this-\u003eSecurity-\u003erequirePost($actions);\n    }\n```\n\nAlso if you require all requests to be through secured connection, you can configure the whole controller, or specific actions to redirect all requests to HTTPS instead of HTTP.\n\n```php\n    // NotesController\n\n    public function beforeAction(){\n\n        parent::beforeAction();\n\n        $actions = ['create', 'delete'];\t// specific action methods\t\n        $actions = ['*'];\t\t        \t// all action methods\n\n        $this-\u003eSecurity-\u003erequireSecure($actions);\n    }\n```\n#### Domain Validation\u003ca name=\"referer\"\u003e\u003c/a\u003e\n\nIt checks \u0026 validates if request is coming from the same domain. Although they can be faked, It's good to keep them as part of our security layers.\n\n#### Form Tampering\u003ca name=\"form-tampering\"\u003e\u003c/a\u003e\n\nValidate submitted form coming from POST request. The pitfall of this method is you need to define the expected form fields, or data that will be sent with POST request. \n\nBy default, the framework will validate for form tampering when POST request is made, and it will make sure the CSRF token is passed with the form fields. In this situation, if you didn't pass the CSRF token, it will be considered as a Security thread.\n\n+ Unknown fields cannot be added to the form.\n+ Fields cannot be removed from the form.\n\n```php\n    // NotesController\n\n    public function beforeAction(){\n\n        parent::beforeAction();\n\n        $action = $this-\u003erequest-\u003eparam('action');\n        $actions = ['create', 'delete'];\n\n        $this-\u003eSecurity-\u003erequireAjax($actions);\n        $this-\u003eSecurity-\u003erequirePost($actions);\n\n        switch($action){\n            case \"create\":\n                $this-\u003eSecurity-\u003econfig(\"form\", [ 'fields' =\u003e ['note_text']]);\n                break;\n            case \"delete\":\n            \t// If you want to disable validation for form tampering\n            \t// $this-\u003eSecurity-\u003econfig(\"validateForm\", false);\n                $this-\u003eSecurity-\u003econfig(\"form\", [ 'fields' =\u003e ['note_id']]);\n                break;\n        }\n    }\n```\n\n#### CSRF Tokens\u003ca name=\"csrf\"\u003e\u003c/a\u003e\nCSRF Tokens are important to validate the submitted forms, and to make sure they aren't faked. A hacker can trick the user to make a request to a website, or click on a link, and so on.\n\nThey are valid for a certain duration(\u003e= 1 day), then it will be regenerated and stored in user's session.\n\nCSRF validation is disabled by default. If you want to validate the CSRF token, then assign ```validateCsrfToken``` to ```true``` as shown in the example below. CSRF validation will be forced when request is POST and form tampering is enabled. \n\nNow, You do not need to manually verify the CSRF token on every requests. The _Security_ Component will verify token in the request versus the token stored in the session.\n\n```php\n    // NotesController\n\n    public function beforeAction(){\n\n        parent::beforeAction();\n\n\t\t$action = $this-\u003erequest-\u003eparam('action');\n\t\t$actions = ['index'];\n\n        $this-\u003eSecurity-\u003erequireGet($actions);\n\n        switch($action){\n            case \"index\":\n                $this-\u003eSecurity-\u003econfig(\"validateCsrfToken\", true);\n                break;\n        }\n    }\n```\n\nCSRF tokens are generated per session. You can either add a hidden form field, or in the URL as query parameter.\n\n**Form**\n\n``` \u003cinput type=\"hidden\" name=\"csrf_token\" value=\"\u003c?= Session::generateCsrfToken(); ?\u003e\" /\u003e ``` \n\n**URL**\n\n``` \u003ca href=\"\u003c?= PUBLIC_ROOT . \"?csrf_token=\" . urlencode(Session::generateCsrfToken()); ?\u003e\"\u003eLink\u003c/a\u003e ```\n\n**JavaScript**\n\nYou can also assign the CSRF token to a javascript variable. \n\n```\u003cscript\u003econfig = \u003c?= json_encode(Session::generateCsrfToken()); ?\u003e;\u003c/script\u003e``` \n\n#### htacess\u003ca name=\"htaccess\"\u003e\u003c/a\u003e\n\n+ All requests will be redirected to ```index.php``` in public root folder. \n+ Block directory traversal/browsing\n+ Deny access to app directory(Althought it's not needed if you setup the application correctly)\n\n### Turn on/off Components(Middlewares) \u003ca name=\"turn-on-off-components\"\u003e\u003c/a\u003e\nSometimes you need to have a control on these components, such as when want to have a Controller without Authentication or Authorization, or a Security component is enabled. This can be done by override ```initialize()``` method inside your Controller class, and load only needed Components.\n\n**Example 1**: Don't load any component, no authentication or authorization, or security validations.\n```php\npublic function initialize(){\n\n\t$this-\u003eloadComponents([]);\n}\n```\n**Example 2**: Load Security, \u0026 Auth component, but don't authenticate and authorize, just in case you want to use the Auth component inside the action methods. [LoginController](https://github.com/OmarElGabry/miniPHP/blob/master/app/controllers/LoginController.php#L60) is an example on **how to access a page without require a logged-in user**.\n```php\npublic function initialize(){\n\t$this-\u003eloadComponents([ \n\t    \t'Auth',\n\t    \t'Security'\n\t    ]);\n}\n ```\n**Example 3**: Load Security, \u0026 Auth component, and authenticate user \u0026 authorize for the current controller. This is the default behavior in the [core/Controller](https://github.com/OmarElGabry/miniPHP/blob/master/app/core/Controller.php#L137) Class\n```php\npublic function initialize(){\n\t$this-\u003eloadComponents([\n\t\t'Auth' =\u003e [\n\t\t\t'authenticate' =\u003e ['User'],\n\t\t\t'authorize' =\u003e ['Controller']\n\t\t],\n\t\t'Security'\n\t    ]);\n}\n``` \n\n## Views \u003ca name=\"views\"\u003e\u003c/a\u003e\n\nInside the action method you can make a call to model to get some data, and/or render pages inside _views_ folder\n\n```php\n  //  NotesController\n  \n  public function index(){\n \n\t// render full page with layout(header and footer)\n\t$this-\u003eview-\u003erenderWithLayouts(Config::get('VIEWS_PATH') . \"layout/default/\", Config::get('VIEWS_PATH') . 'notes/index.php');\n\t\n\t// render page without layout\n\t$this-\u003eview-\u003erender(Config::get('VIEWS_PATH') . 'notes/note.php');\n\t\n\t// get the rendered page\n\t$html = $this-\u003eview-\u003erender(Config::get('VIEWS_PATH') . 'notes/note.php');\n\t\n\t// render a json view\n\t$this-\u003eview-\u003erenderJson(array(\"data\" =\u003e $html));\n  }\n```\n\n## Models \u003ca name=\"models\"\u003e\u003c/a\u003e\n\u003e In MVC, the model represents the information (the data) and the business rules; the view contains elements of the user interface such as text, form inputs; and the controller manages the communication between the model and the view.\n[Source](http://www.yiiframework.com/doc/guide/1.1/en/basics.mvc)\n\nAll operations like create, delete, update, and validation are implemented in model classes.\n\n```php\n   // NotesController\n\n    public function create(){\n    \n\t\t// get content of note submitted to a form\n\t\t// then pass the content along with the current user to Note class\n\t\t$content  = $this-\u003erequest-\u003edata(\"note_text\");\n\t\t$note     = $this-\u003enote-\u003ecreate(Session::getUserId(), $content);\n        \n        if(!$note){\n            $this-\u003eview-\u003erenderErrors($this-\u003enote-\u003eerrors());\n        }else{\n            return $this-\u003eredirector-\u003eroot(\"Notes\");\n        }\n    }\n```\n\n**In Notes Model**\n\n```php\n   // Notes Model\n\n    public function create($userId, $content){\n    \n    \t// using validation class(see below)\n        $validation = new Validation();\n        if(!$validation-\u003evalidate(['Content'   =\u003e [$content, \"required|minLen(4)|maxLen(300)\"]])) {\n            $this-\u003eerrors = $validation-\u003eerrors();\n            return false;\n        }\n        \n        // using database class to insert new note\n        $database = Database::openConnection();\n        $query    = \"INSERT INTO notes (user_id, content) VALUES (:user_id, :content)\";\n        $database-\u003eprepare($query);\n        $database-\u003ebindValue(':user_id', $userId);\n        $database-\u003ebindValue(':content', $content);\n        $database-\u003eexecute();\n        \n        if($database-\u003ecountRows() !== 1){\n            throw new Exception(\"Couldn't create note\");\n        }\n        \n        return true;\n     }\n```\n\n## Login\u003ca name=\"login\"\u003e\u003c/a\u003e\nUsing the framework, you would probably do login, register, and logout. These actions are implemented in _app/models/Login_ \u0026 _app/controllers/LoginController_. In most situations, you won't need to modify anything related to login actions, just understand the behaviour of the framework. \n\n**NOTE** If you don't have SSL, you would better want to encrypt data manually at Client Side, If So, read [this](http://stackoverflow.com/questions/3715920/about-password-hashing-system-on-client-side) and also [this](http://stackoverflow.com/questions/4121629/password-encryption-at-client-side?lq=1).\n\n### User Verification\u003ca name=\"user-verification\"\u003e\u003c/a\u003e\nWhenever the user registers, An email will be sent with token concatenated with encrypted user id. This token will be expired after 24 hour. It's much better to expire these tokens, and re-use the registered email if they are expired.\n\n**Passwords** are hashed using the latest algorithms in PHP v5.5\n```php\n$hashedPassword = password_hash($password, PASSWORD_DEFAULT, array('cost' =\u003e Config::get('HASH_COST_FACTOR')));\n```\n\n### Forgotten Password\u003ca name=\"forgotten-password\"\u003e\u003c/a\u003e\nIf user forgot his password, he can restore it. The same idea of expired tokens goes here. \n\nIn addition, block user for certain duration(\u003e= 10min) if he exceeded number of forgotten passwords attempts(5) during a certain duration(\u003e= 10min).\n\n#### Brute Force Attack\u003ca name=\"brute-force\"\u003e\u003c/a\u003e\nThrottling brute-force attacks is when a hacker tries all possible input combination until he finds the correct password.\n\nSolution:\n+ Block failed logins, So, if a user exceeded number of failed logins(5) during certain duration(\u003e= 10min), the email will be blocked for duration(\u003e= 10min).\n+ Blocking will be for emails even these emails aren't stored in our database, meaning for non-registered users.\n+ Require **Strong** passwords\n\t- At least one lowercase character\n\t- At least one uppercase character\n\t- At least one special character\n\t- At least one number\n\t- Min Length is 8 characters\n\n### Captcha\u003ca name=\"captcha\"\u003e\u003c/a\u003e\nCAPTCHAs are particularly effective in preventing automated logins. Using [Captcha](https://github.com/Gregwar/Captcha) an awesome PHP Captcha library.\n\n### Block IP Address\u003ca name=\"block-ip\"\u003e\u003c/a\u003e\nBlocking IP Addresses is the last solution to think about. IP Address will be blocked if the same IP failed to login multiple times using different credentials(\u003e=10).\n\n## Database\u003ca name=\"database\"\u003e\u003c/a\u003e\nPHP Data Objects (PDO) is used for preparing and executing database queries. Inside ```Database``` Class, there are various methods that hides complexity and let's you instantiate database object, prepare, bind, and execute in few lines.\n\n+ SQL Injection\n\t- Using prepared statements will prevent SQL Injection.\n+ Limit Privileges\n\t- Don't use _root_ user, Create a new one instead.\n\t- Always assign limited privileges to current database user\n\t- ```SELECT, INSERT, UPDATE, DELETE ``` are enough for users\n\t- For backups, It's recommended to use another database user with more privileges. These privileges needed for [mysqldump](https://dev.mysql.com/doc/refman/5.1/en/mysqldump.html) are mentioned in ```Admin``` Class.\n+ UTF-8\n\t- For complete UTF-8 support, you need to use ```utf8mb4 ```on database level.\n\t- MySQL’s ```utf8``` charset only store UTF-8 encoded symbols that consist of one to three bytes. But, It can't for symbols with four bytes. \n\t- Here, charset is ```utf8```. But, if you want to upgrade to ```utf8mb4 ``` follow these links:\n\t\t- [Link 1](https://mathiasbynens.be/notes/mysql-utf8mb4) \u0026 [Link 2](https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-upgrading.html)\n\t\t- Don't forget to change **charset** in _app/config/config.php_ to ```utf8mb4 ```\n\n## Encryption\u003ca name=\"encryption\"\u003e\u003c/a\u003e\n``` Encryption ``` Class is responsible for encrypting and decryption of data. Encryption is applied to things like cookies, User ID, Post ID, ..etc. Encrypted strings are authenticated and they are different every time you encrypt. \n\n## Validation\u003ca name=\"validation\"\u003e\u003c/a\u003e\nValidation is a small library for validating user inputs. All validation rules are inside ``` Validation ``` Class.\n\n#### Usage\n```php\n\n$validation = new Validation();\n\n// there are default error messages for each rule\n// but, you still can define your custom error message\n$validation-\u003eaddRuleMessage(\"emailUnique\", \"The email you entered is already exists\");\n\nif(!$validation-\u003evalidate([\n    \"User Name\" =\u003e [$name, \"required|alphaNumWithSpaces|minLen(4)|maxLen(30)\"],\n    \"Email\" =\u003e [$email, \"required|email|emailUnique|maxLen(50)\"],\n    'Password' =\u003e [$password,\"required|equals(\".$confirmPassword.\")|minLen(6)|password\"],\n    'Password Confirmation' =\u003e [$confirmPassword, 'required']])) {\n\n    var_dump($validation-\u003eerrors());\n}\n```\n\n## Errors and Exceptions\u003ca name=\"errors-exceptions\"\u003e\u003c/a\u003e\n``` Handler``` Class is responsible for handling all exceptions and errors. It will use [Logger](#logger) to log errors. Error reporting is turned off by default, because every error will be logged and saved in  _app/logs/log.txt_.\n\nIf error encountered or exception was thrown, the application will show System Internal Error(500).\n\n### Configurations(php.ini)\n+ Turn Off display errors\n+ Turn Off log errors if not needed\n\n## Logger\u003ca name=\"logger\"\u003e\u003c/a\u003e\nA place where you can log anything and save it to _app/log/log.txt_. You can write any failures, errors, exceptions, or any other malicious actions or attacks.\n\n```php\nLogger::log(\"COOKIE\", self::$userId . \" is trying to login using invalid cookie\", __FILE__, __LINE__);\n```\n\n## Email\u003ca name=\"email\"\u003e\u003c/a\u003e\nEmails are sent using [PHPMailer](https://github.com/PHPMailer/PHPMailer) via SMTP, another library for sending emails. You shouldn't use ```mail()``` function of PHP.\n\n## Configurations\u003ca name=\"configurations\"\u003e\u003c/a\u003e\nIn _app/config_, there are two files, one called _config.php_ for main application configurations, and another one for javascript called _javascript.php_. The javascript configurations will be then assigned to a javascript variable in your _footer.php_.\n\n## JavaScript \u003ca name=\"js\"\u003e\u003c/a\u003e\nIn order to send request and recieve a respond, you may depend on Ajax calls to do so. This framework is heavily depends on ajax requests to perform actions, but, you still can do the same thing for normal requests with just small tweaks.\n\n#### In _public/main.js_\n\n**config** object is assigned to key-value pairs in [footer.php](https://github.com/OmarElGabry/miniPHP/blob/master/app/views/layout/default/footer.php). These key-value pairs can be added in server-side code using ```Config::setJsConfig('key', \"value\");```, which will be assigned then to _config_ object.\n\n**ajax** A namespace that has two main functions for sending ajax request. One for normal ajax calls, and another for for uploading files.\n\n**helpers** A namespace that has variety of functions display errors, serialize, redirect, encodeHTML, and so on\n\n**app** A namespace that's used to initalize the whole javascript events for the current page\n\n**events** A namespace that's used to declare all of events that may occure, like when user clicks on a link to create, delete or update.\n\n## Application(Demo) \u003ca name=\"app-demo\"\u003e\u003c/a\u003e\n### Intro\u003ca name=\"intro-demo\"\u003e\u003c/a\u003e\nIn order to show how to use the framework in a real-life situation, the framework comes with implementation for features like Manage User Profile Management, Dashboard, News Feed, Upload \u0026 Download Files, Posts \u0026 Comments, Pagination, Admin panel, Manage System Backups, Notificatons, Report Bugs, ...etc.\n\n### Installation\u003ca name=\"installation-demo\"\u003e\u003c/a\u003e\nSteps:\n\n1. Edit configuration file in _app/config/config.php_ with your credentials\n\n2. Execute SQL queries in __installation_ directory in order\n\n3. Login\n\t+ Admin:\n\t\t+ Email: admin@demo.com\n\t\t+ Password: 12345\n\t+ Normal User:\n\t\t+ Email: user@demo.com\n\t\t+ Password: 12345\n\n**EMAIL SETUP** \n\nYou need to configure your SMTP account data in _app/config/config.php_. **But**, If you don't have SMTP account, then you save emails in _app/logs/log.txt_ using Logger. \n\nTo do that, In [core/Email](https://github.com/OmarElGabry/miniPHP/blob/master/app/core/Email.php#L78), comment ```$mail-\u003eSend()``` \u0026 uncomment ```Logger::log(\"EMAIL\", $mail-\u003eBody);``` \n\n### User Profile\u003ca name=\"profile\"\u003e\u003c/a\u003e\nEvery user can change his name, email, password. Also upload profile picture (i.e. initially assigned to default.png).\n\n#### Update \u0026 Revoke User Email\u003ca name=\"update-revoke-user-email\"\u003e\u003c/a\u003e\nWhenever user asks to change his email, a notification will be sent to user's old email, and the new one.\n\nThe notification sent to old email is giving the user the chance to revoke email change, while the notification sent to new email is asking for confirmation. User can still login with his old email until he confirms the change.\n\nThis is done in ```UserController```, In methods ```updateProfileInfo()```, ```revokeEmail()```, \u0026 ```updateEmail()```. In most situations, you won't need to modify the behavior of these methods.\n\n### Files\u003ca name=\"files\"\u003e\u003c/a\u003e\nYou can upload and download files.\n\n#### Upload\n+ All uploaded files are out of root public, so, they aren't accessible by anyone\n+ Validate against HTTP POST uploads, MIME, Size, Image dimension\n+ Setting file permission to avoid executable files\n+ Sanitizing file names\n+ Progress bar(no-plugins)\n\n#### Download\n+ Every file will have hashed version of it's name, this hashed name will be exposed to users. \n+ The hashed name = hash(original filename . extension). So, download link will look something like this: _http://miniPHP/downloads/download/b989f733f948e8a4b8b700e1_\n\n#### Configurations(php.ini)\n+ Set ```file_uploads``` to true\n+ Set ```upload_max_filesize, max_file_uploads, post_max_size```\n\t- Check [documentation](http://php.net/manual/en/ini.core.php#ini.post-max-size) to know how to assign proper values for each.\n\n### News Feeds, Posts \u0026 Comments \u003ca name=\"newsfeed-posts-comments\"\u003e\u003c/a\u003e\n\nThink of News Feed as tweets in twitter, and in Posts like when you open an Issue in Github.\n\nThey are implemented on the top of this framework. \n+ They are useful to show \u0026 apply some concepts like **Pagination**, \n+ How can you edit \u0026 delete in place(secured way), \n+ How can you manage permissions for who can create, edit, update and delete, and so forth.\n\n### Admin\u003ca name=\"admin\"\u003e\u003c/a\u003e\nAdmins can perform actions where normal users can't. They can delete, edit, create any newsfeed, post, or comment. Also they have control over all user profiles, create \u0026 restore backups.\n\n#### Users\u003ca name=\"users\"\u003e\u003c/a\u003e\nOnly admins have access to see all registered users. They can delete, edit their info.\n\n#### Backups\u003ca name=\"backups\"\u003e\u003c/a\u003e\nIn most of the situations, you will need to create backups for the system, and restore them whenever you want.\n\nThis is done by using [mysqldump](https://dev.mysql.com/doc/refman/5.1/en/mysqldump.html) to create and restore backups. All backups will be stored in _app/backups_.\n\n### Notifications\u003ca name=\"notifications\"\u003e\u003c/a\u003e\nDid you see the red notifications on facebook, or the blue one on twitter?. The same idea is here. But, It's implemented using triggers instead. Triggers are defined in __installation/triggers.sql_.\n\nSo, whenever user creates a new newsfeed, post, or upload a file, this will increment the count for all other users, and will display a red notification in navigation bar.\n\n### Report Bugs\u003ca name=\"bugs\"\u003e\u003c/a\u003e\nUsers can report Bugs, Features \u0026 Enhancements. Once they submitted the form, an email will be sent to ```ADMIN_EMAIL``` defined in _app/config/config.php_\n\n## ToDo Application\u003ca name=\"todo\"\u003e\u003c/a\u003e\nLet's say you want to build a simple ToDo Application. Here, I will go step by step on how to create a ToDo App using the framework with \u0026 without Ajax calls.\n\n(1) If you followed the installtion setup steps above, you shouldn't have any problem with creating initial user accounts.\n\n(2) Create a table with id as INT, content VARCHAR, user_id as Foreign Key to ```users``` table\n\n```sql\nCREATE TABLE `todo` (\n\t `id` int(11) NOT NULL AUTO_INCREMENT,\n\t `user_id` int(11) NOT NULL,\n\t `content` varchar(512) NOT NULL,\n\t PRIMARY KEY (`id`),\n\t FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;\n```\n\n(3) Create TodoController\n\nCreate a file called ```TodoController.php``` inside _app/controllers_\n\n```php\n\nclass TodoController extends Controller{\n\n    // override this method to perform any logic before calling action method as explained above\n    public function beforeAction(){\n\n        parent::beforeAction();\n\n        // define the actions in this Controller\n        $action = $this-\u003erequest-\u003eparam('action');\n\n        // restrict the request to action methods\n        // $this-\u003eSecurity-\u003erequireAjax(['create', 'delete']);\n        $this-\u003eSecurity-\u003erequirePost(['create', 'delete']);\n\n        // define the expected form fields for every action if exist\n        switch($action){\n            case \"create\":\n                // you can exclude form fields if you don't care if they were sent with form fields or not\n                $this-\u003eSecurity-\u003econfig(\"form\", [ 'fields' =\u003e ['content']]);\n                break;\n            case \"delete\":\n\t\t\t\t// If you want to disable validation for form tampering\n\t\t\t\t// $this-\u003eSecurity-\u003econfig(\"validateForm\", false);\n                $this-\u003eSecurity-\u003econfig(\"form\", [ 'fields' =\u003e ['todo_id']]);\n                break;\n        }\n    }\n\n    public function index(){\n\n        $this-\u003eview-\u003erenderWithLayouts(Config::get('VIEWS_PATH') . \"layout/todo/\", Config::get('VIEWS_PATH') . 'todo/index.php');\n    }\n\n    public function create(){\n\n        $content  = $this-\u003erequest-\u003edata(\"content\");\n        $todo     = $this-\u003etodo-\u003ecreate(Session::getUserId(), $content);\n\n        if(!$todo){\n\n            // in case of normal post request\n            Session::set('errors', $this-\u003etodo-\u003eerrors());\n            return $this-\u003eredirector-\u003eroot(\"Todo\");\n\n            // in case of ajax\n            // $this-\u003eview-\u003erenderErrors($this-\u003etodo-\u003eerrors());\n\n        }else{\n\n            // in case of normal post request\n            Session::set('success', \"Todo has been created\");\n            return $this-\u003eredirector-\u003eroot(\"Todo\");\n\n            // in case of ajax\n            // $this-\u003eview-\u003erenderJson(array(\"success\" =\u003e \"Todo has been created\"));\n        }\n    }\n\n    public function delete(){\n\n        $todoId = Encryption::decryptIdWithDash($this-\u003erequest-\u003edata(\"todo_id\"));\n        $this-\u003etodo-\u003edelete($todoId);\n\n        // in case of normal post request\n        Session::set('success', \"Todo has been deleted\");\n        return $this-\u003eredirector-\u003eroot(\"Todo\");\n\n        // in case of ajax\n        // $this-\u003eview-\u003erenderJson(array(\"success\" =\u003e \"Todo has been deleted\"));\n    }\n\n    public function isAuthorized(){\n\n        $action = $this-\u003erequest-\u003eparam('action');\n        $role = Session::getUserRole();\n        $resource = \"todo\";\n\n        // only for admins\n        Permission::allow('admin', $resource, ['*']);\n\n        // only for normal users\n        Permission::allow('user', $resource, ['delete'], 'owner');\n\n        $todoId = $this-\u003erequest-\u003edata(\"todo_id\");\n\n        if(!empty($todoId)){\n            $todoId = Encryption::decryptIdWithDash($todoId);\n        }\n\n        $config = [\n            \"user_id\" =\u003e Session::getUserId(),\n            \"table\" =\u003e \"todo\",\n            \"id\" =\u003e $todoId];\n\n        return Permission::check($role, $resource, $action, $config);\n    }\n}\n```\n\n(4) Create Note Model Class called ```Todo.php``` in _app/models_\n\n```php\nclass Todo extends Model{\n\n    public function getAll(){\n\n        $database = Database::openConnection();\n        $query  = \"SELECT todo.id AS id, users.id AS user_id, users.name AS user_name, todo.content \";\n        $query .= \"FROM users, todo \";\n        $query .= \"WHERE users.id = todo.user_id \";\n\n        $database-\u003eprepare($query);\n        $database-\u003eexecute();\n        $todo = $database-\u003efetchAllAssociative();\n\n        return $todo;\n     }\n\n    public function create($userId, $content){\n    \n    \t// using validation class\n        $validation = new Validation();\n        if(!$validation-\u003evalidate(['Content'   =\u003e [$content, \"required|minLen(4)|maxLen(300)\"]])) {\n            $this-\u003eerrors = $validation-\u003eerrors();\n            return false;\n        }\n        \n        // using database class to insert new todo\n        $database = Database::openConnection();\n        $query    = \"INSERT INTO todo (user_id, content) VALUES (:user_id, :content)\";\n        $database-\u003eprepare($query);\n        $database-\u003ebindValue(':user_id', $userId);\n        $database-\u003ebindValue(':content', $content);\n        $database-\u003eexecute();\n        \n        if($database-\u003ecountRows() !== 1){\n            throw new Exception(\"Couldn't create todo\");\n        }\n        \n        return true;\n     }\n  \n    public function delete($id){\n\n        $database = Database::openConnection();\n        $database-\u003edeleteById(\"todo\", $id);\n\n        if($database-\u003ecountRows() !== 1){\n            throw new Exception (\"Couldn't delete todo\");\n        }\n    }\n }\n```\n\n(5) Inside _views/_\n\n(a) Create ```header.php``` \u0026 ```footer.php```  inside _views/layout/todo_\n\n```php\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\n\u003chead\u003e\n\t\t\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n    \u003cmeta name=\"description\" content=\"mini PHP\"\u003e\n    \u003cmeta name=\"author\" content=\"mini PHP\"\u003e\n\n    \u003ctitle\u003emini PHP\u003c/title\u003e\n\n    \u003c!-- Stylesheets --\u003e\n    \u003clink rel=\"stylesheet\" href=\"\u003c?= PUBLIC_ROOT;?\u003ecss/bootstrap.min.css\"\u003e\n    \u003clink rel=\"stylesheet\" href=\"\u003c?= PUBLIC_ROOT;?\u003ecss/sb-admin-2.css\"\u003e\n    \u003clink rel=\"stylesheet\" href=\"\u003c?= PUBLIC_ROOT;?\u003ecss/font-awesome.min.css\" rel=\"stylesheet\" type=\"text/css\"\u003e\n\t\n    \u003c!-- Styles for ToDo Application --\u003e\n    \u003cstyle\u003e\n        .todo_container{\n            width:80%; \n            margin: 0 auto; \n            margin-top: 5%\n        }\n        #todo-list li{ \n            list-style-type: none; \n            border: 1px solid #e7e7e7;\n            padding: 3px;\n            margin: 3px;\n        }\n        #todo-list li:hover{\n            background-color: #eee;\n        }\n        form button{\n            float:right;\n            margin: 3px;\n        }\n        form:after{\n            content: '';\n            display: block;\n            clear: both;\n        }\n    \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n```\n\n```php\n\t\u003c!-- footer --\u003e\n\n\t\u003cscript src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js\"\u003e\u003c/script\u003e\n\t\u003c!--\u003cscript src=\"\u003c?= PUBLIC_ROOT; ?\u003ejs/jquery.min.js\"\u003e\u003c/script\u003e--\u003e\n\t\u003cscript src=\"\u003c?= PUBLIC_ROOT; ?\u003ejs/bootstrap.min.js\"\u003e\u003c/script\u003e\n\t\u003cscript src=\"\u003c?= PUBLIC_ROOT; ?\u003ejs/sb-admin-2.js\"\u003e\u003c/script\u003e\n\t\u003cscript src=\"\u003c?= PUBLIC_ROOT; ?\u003ejs/main.js\"\u003e\u003c/script\u003e\n\n        \u003c!-- Assign CSRF Token to JS variable --\u003e\n\t\t\u003c?php Config::setJsConfig('csrfToken', Session::generateCsrfToken()); ?\u003e\n        \u003c!-- Assign all configration variables --\u003e\n\t\t\u003cscript\u003econfig = \u003c?= json_encode(Config::getJsConfig()); ?\u003e;\u003c/script\u003e\n        \u003c!-- Run the application --\u003e\n        \u003cscript\u003e$(document).ready(app.init());\u003c/script\u003e\n        \n        \u003c?php Database::closeConnection(); ?\u003e\n\t\u003c/body\u003e\n\u003c/html\u003e\n```\n\n(b) Inside _views/_ Create todo folder that will have ```index.php```, which will contain our todo list. \n\n```php\n\u003cdiv class=\"todo_container\"\u003e\n\n\u003ch2\u003eTODO Application\u003c/h2\u003e\n\n\u003c!-- in case of normal post request  --\u003e\n\u003cform action= \"\u003c?= PUBLIC_ROOT . \"Todo/create\" ?\u003e\"  method=\"post\"\u003e\n    \u003clabel\u003eContent \u003cspan class=\"text-danger\"\u003e*\u003c/span\u003e\u003c/label\u003e\n    \u003ctextarea name=\"content\" class=\"form-control\" required placeholder=\"What are you thinking?\"\u003e\u003c/textarea\u003e\n    \u003cinput type='hidden' name = \"csrf_token\" value = \"\u003c?= Session::generateCsrfToken(); ?\u003e\"\u003e\n    \u003cbutton type=\"submit\" name=\"submit\" value=\"submit\" class=\"btn btn-success\"\u003eCreate\u003c/button\u003e\n\u003c/form\u003e\n\n\n\u003c!-- in case of ajax request  \n\u003cform action= \"#\" id=\"form-create-todo\" method=\"post\"\u003e\n    \u003clabel\u003eContent \u003cspan class=\"text-danger\"\u003e*\u003c/span\u003e\u003c/label\u003e\n    \u003ctextarea name=\"content\" class=\"form-control\" required placeholder=\"What are you thinking?\"\u003e\u003c/textarea\u003e\n    \u003cbutton type=\"submit\" name=\"submit\" value=\"submit\" class=\"btn btn-success\"\u003eCreate\u003c/button\u003e\n\u003c/form\u003e\n--\u003e\n\n\u003cbr\u003e\n\u003c?php \n\n// display success or error messages in session\nif(!empty(Session::get('success'))){\n    echo $this-\u003erenderSuccess(Session::getAndDestroy('success'));\n}else if(!empty(Session::get('errors'))){\n    echo $this-\u003erenderErrors(Session::getAndDestroy('errors'));\n}\n\n?\u003e\n\n\u003cbr\u003e\u003chr\u003e\u003cbr\u003e\n\n\u003cul id=\"todo-list\"\u003e\n\u003c?php \n    $todoData = $this-\u003econtroller-\u003etodo-\u003egetAll();\n    foreach($todoData as $todo){ \n?\u003e\n        \u003cli\u003e\n            \u003cp\u003e \u003c?= $this-\u003eautoLinks($this-\u003eencodeHTMLWithBR($todo[\"content\"])); ?\u003e\u003c/p\u003e\n\n            \u003c!-- in case of normal post request --\u003e\n            \u003cform action= \"\u003c?= PUBLIC_ROOT . \"Todo/delete\" ?\u003e\" method=\"post\"\u003e\n                \u003cinput type='hidden' name= \"todo_id\" value=\"\u003c?= \"todo-\" . Encryption::encryptId($todo[\"id\"]);?\u003e\"\u003e\n                \u003cinput type='hidden' name = \"csrf_token\" value = \"\u003c?= Session::generateCsrfToken(); ?\u003e\"\u003e\n                \u003cbutton type=\"submit\" name=\"submit\" value=\"submit\" class=\"btn btn-xs btn-danger\"\u003eDelete\u003c/button\u003e\n            \u003c/form\u003e\n\n\n            \u003c!-- in case of ajax request \n            \u003cform class=\"form-delete-todo\" action= \"#\"  method=\"post\"\u003e\n                \u003cinput type='hidden' name= \"todo_id\" value=\"\u003c?= \"todo-\" . Encryption::encryptId($todo[\"id\"]);?\u003e\"\u003e\n                \u003cbutton type=\"submit\" name=\"submit\" value=\"submit\" class=\"btn btn-xs btn-danger\"\u003eDelete\u003c/button\u003e\n            \u003c/form\u003e\n             --\u003e\n        \u003c/li\u003e\n    \u003c?php } ?\u003e\n\u003c/ul\u003e\n\n\u003c/div\u003e\n```\n\n(6) JavaScript code to send ajax calls, and handle respond\n\n```js\n\n// first, we need to initialize the todo events whenever the application initalized\n// the app.init() is called in footer.php, see views/layout/todo/footer.php\n\nvar app = {\n    init: function (){\n    \n    \tevents.todo.init();\n    }\n};\n\n// inside var events = {....} make a new key called \"todo\" \nvar events = {\n\t// ....\n\ttodo:{\n\t        init: function(){\n\t            events.todo.create();\n\t            events.todo.delete();\n\t        },\n\t        create: function(){\n\t            $(\"#form-create-todo\").submit(function(e){\n\t                e.preventDefault();\n\t                ajax.send(\"Todo/create\", helpers.serialize(this), createTodoCallBack, \"#form-create-todo\");\n\t            });\n\t\n\t            function createTodoCallBack(PHPData){\n\t                if(helpers.validateData(PHPData, \"#form-create-todo\", \"after\", \"default\", \"success\")){\n\t                    alert(PHPData.success + \" refresh the page to see the results\");\n\t                }\n\t            }\n\t        },\n\t        delete: function(){\n\t            $(\"#todo-list form.form-delete-todo\").submit(function(e){\n\t                e.preventDefault();\n\t                if (!confirm(\"Are you sure?\")) { return; }\n\t                \n\t                var cur_todo = $(this).parent();\n\t                ajax.send(\"Todo/delete\", helpers.serialize(this), deleteTodoCallBack, cur_todo);\n\t                \n\t                function deleteTodoCallBack(PHPData){\n\t                    if(helpers.validateData(PHPData, cur_todo, \"after\", \"default\", \"success\")){\n\t                        $(cur_todo).remove();\n\t                        alert(PHPData.success);\n\t                    }\n\t                }\n\t            });\n\t\t}\n\t}\n}\n```\n\n### Support \u003ca name=\"support\"\u003e\u003c/a\u003e\nI've written this script in my free time during my studies. This is for free, unpaid. I am saying this because I've seen many developers acts very rude towards any software, and their behavior is really frustrating. I don't know why?! Everyone tends to complain, and saying harsh words. I do accept the feedback, but, in a good and respectful manner.\n\nThere are many other scripts online for purchase that does the same thing(if not less), and their authors are earning good money from it, but, I choose to keep it public, available for everyone.\n\nIf you learnt something, or I saved your time, please support the project by spreading the word.\n\n### Contribute \u003ca name=\"contribute\"\u003e\u003c/a\u003e\n\nContribute by creating new issues, sending pull requests on Github or you can send an email at: omar.elgabry.93@gmail.com\n\n### Dependencies \u003ca name=\"dependencies\"\u003e\u003c/a\u003e\n+ [PHPMailer](https://github.com/PHPMailer/PHPMailer)\n+ [Captcha](https://github.com/Gregwar/Captcha)\n+ [Theme SB Admin 2](https://github.com/IronSummitMedia/startbootstrap-sb-admin-2)\n\n### License \u003ca name=\"license\"\u003e\u003c/a\u003e\nBuilt under [MIT](http://www.opensource.org/licenses/mit-license.php) license.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOmarElgabry%2FminiPHP","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FOmarElgabry%2FminiPHP","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOmarElgabry%2FminiPHP/lists"}