File | Date | Author | Commit |
---|---|---|---|
_installation | 2015-07-06 |
![]() |
[997078] separate database, tables, and triggers queries |
app | 2015-07-06 |
![]() |
[b28d78] Initial commit |
public | 2015-07-06 |
![]() |
[b28d78] Initial commit |
vendor | 2015-07-06 |
![]() |
[b28d78] Initial commit |
.htaccess | 2015-07-06 |
![]() |
[b28d78] Initial commit |
LICENSE | 2015-07-06 |
![]() |
[efa8fc] Initial commit |
README.md | 2015-07-10 |
![]() |
[77a6a6] Update README.md |
composer.json | 2015-07-06 |
![]() |
[b28d78] Initial commit |
composer.lock | 2015-07-06 |
![]() |
[b28d78] Initial commit |
A small, simple PHP MVC framework skeleton that encapsulates a lot of features surrounded with powerful security layers.
miniPHP 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.
It'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.
It's indented to remove the complexity of the frameworks. I've been digging into the internals of some frameworks for a while. Things like authentication, and authorization that you will see here is not something I've invented from the scratch, Some of it, is aggregation of concepts applied already be frameworks, but, built in a much simpler way, So, you can understand it, and take it further.
If you need to build bigger application, and take the advantage of most of the features available in frameworks, you can see CakePHP, Laravel, Symphony.
Either way, I believe 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 features(News Feed, Posts, Files, ...etc) and merge them with these core features.
Steps:
Edit configuration file in app/config/config.php with your credentials
Execute SQL queries in __installation_ directory
Install Composer for dependencies
composer install
Whenever you make a request to the application, it wil be directed to index.php inside public root folder.
So, if you make a request: http://localhost/miniPHP/User/update/412
. This will be splitted and translated into
NOTE
In 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'
.
Then App
Class, Inside splitUrl()
, will split the query string $_GET['url']
intro controller, action method, and any passed arguments to action method.
In App
Class, Inside __construct()
, will instantiate an object from controller class, and make a call to action method.
After the App
Class intantiates controller object, The constructor of Controller
Class will trigger 3 consective events/methods:
initialize()
: Use it to load components, beforeAction()
: Any logic before calling controller's action methodtriggerComponents()
: Trigger startup() method of loaded componentsThe constructor of Controller
Class shouldn't be overridden, instead you can override the 3 methods above in extending classes, and constructor of Controller
Class will call them one after another.
After the constructor finishes it's job, Then, the requested action method will be called, and arguments will be passed(if any)
Inside the action method you can make a call to model to get some data, and/or render pages inside views directory
//Inside UserController
public function index(){
//render full page with layout(header and footer)
echo $this->view->renderWithLayouts(VIEWS_PATH . "layout/", VIEWS_PATH . 'index.php');
//render page
echo $this->view->render(VIEWS_PATH . 'index.php');
}
public function updateProfileInfo(){
$fileData = $this->request->data("file");
$image = $this->user->updateProfilePicture(Session::getUserId(), $fileData);
//json_encode() for ajax calls
echo $this->view->JSONEncode(array("data" => ["src" => PUBLIC_ROOT . "img/profile_pictures/" . $image["basename"]]));
}
Components are pretty much like backbone for controller. They provide reusable logic to be used as part of the controller. Things like Authentication, Authorization, Form Tampering, and Validate CSRF Tokens are implemented inside Components.
It's better to pull these pieces of logic out of controller class, and keep all various tasks and validations inside these Components.
Every component inherits from the base/super class called Component
. Each has a defined task. There are two components, one for Authentication and Authorization, and another one for other Security Issues.
They are very simple to deal with, and they will be called inside controller constructor.
Is user has right credentials?
The AuthComponent takes care of user session.
Do you have the right to access or to perform X action?. The AuthComponent takes care of authorization for each controller. Each controller must implement isAuthorized()
method.
This method will be called by default at the end of controller constructor. What you need to do is to return boolean
value.
So, for example:
//Inside AdminController
public function isAuthorized(){
$role = Session::getUserRole();
if(isset($role) && $role === "admin"){
return true;
}
return false;
}
If 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".
Example on that:
//Inside FilesController
public function isAuthorized(){
$action = $this->request->param('action');
$role = Session::getUserRole();
$resource = "files";
//only for admins
//they are allowed to all actions on $resource
Permission::allow('admin', $resource, ['*']);
//for normal users, they can delete only if the current user is the owner
//Permission class will then check if the current user is owner or not
Permission::allow('user', $resource, ['delete'], 'owner');
$fileId = $this->request->data("file_id");
$config = [
"user_id" => Session::getUserId(),
"table" => "files",
"id" => $fileId
];
//providing the current user's role, $resource, action method, and some configuration data
//Permission class will check based on rules defined above and return boolean value
return Permission::check($role, $resource, $action, $config);
}
Now, you can check authorization based on user's role, and for each action method.
The SecurityComponent takes care of various security tasks and validation.
It'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 initialize()
method, or personally I prefer to keep it inside beforeAction()
method. These methods are inherited from Controller
Class.
//Inside FilesController
public function beforeAction(){
parent::beforeAction();
$action = $this->request->param('action');
$actions = ['create', 'delete'];
$this->Security->requireAjax($actions);
$this->Security->requirePost($actions);
}
Also if you require all requests to be through secured connection, you can configure whole controller, and specific actions to redirect all requests to HTTPS instead of HTTP.
//Inside FilesController
public function beforeAction(){
parent::beforeAction();
$action = $this->request->param('action');
$actions = ['create', 'delete'];
$this->Security->requireSecure($actions);
}
Check & validate 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.
validate submitted form coming from POST request. In case of Ajax request, you can append data along with form values, these values will be validated too.
The pitfall of this method is you need to define the expected form fields, or data that will be sent with POST request.
//Inside FilesController
public function beforeAction(){
parent::beforeAction();
$action = $this->request->param('action');
$actions = ['create', 'delete'];
$this->Security->requireAjax($actions);
$this->Security->requirePost($actions);
switch($action){
case "create":
$this->Security->config("form", [ 'fields' => ['file']]);
break;
case "delete":
$this->Security->config("form", [ 'fields' => ['file_id']]);
break;
}
}
CSRF 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.
They are valid for a certain duration(>= 1 day), then it will be regenerated and stored in user's session.
Here, CSRF tokens are generated per session. You can either add a hidden form field with name = "csrf_token" value = "<?= Session::generateCsrfToken(); ?>"
But, Since all form requests here are made through Ajax calls, Session::generateCsrfToken()
will be assigned to JS variable and will be sent with every ajax request, Instead of adding hidden form value to every form.
index.php
in public root folder. NOTE If you don't have SSL, you would better want to encrypt data manually at Client Side, If So, read this and also this
Whenever 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.
Passwords are hashed using the latest algorithms in PHP v5.5
$hashedPassword = password_hash($password, PASSWORD_DEFAULT, array('cost' => "10"));
If user forgot his password, he can restore it. The same idea of expired tokens goes here.
In addition, block user for certain duration(>= 10min) if he exceeded number of forgotten passwords attempts(5) during a certain duration(>= 10min).
Throttling brute-force attacks is when a hacker tries all possible input combination until he finds the correct password.
Solution:
+ Block failed logins, So, if a user exceeded number of failed logins(5) during certain duration(>= 10min), the email will be blocked for duration(>= 10min).
+ Blocking will be for emails even these emails aren't stored in our database, meaning for non-registered users.
+ Require Strong passwords
- At least one lowercase character
- At least one uppercase character
- At least one special character
- At least one number
- Min Length is 8 characters
CAPTCHAs are particularly effective in preventing automated logins. I am using Captcha an awesome PHP Captcha library.
Blocking IP Addresses is the last solution to think about. IP Address will be blocked if the same IP failed to login multiple times(>=10) using different credentials(emails).
PHP 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.
SELECT, INSERT, UPDATE, DELETE
are enough for usersAdmin
Class.utf8mb4
on database level.utf8
charset only store UTF-8 encoded symbols that consist of one to three bytes. But, It can't for symbols with four bytes. utf8
. But, if you want to upgrade to utf8mb4
follow these links:
Encryption
Class is responsible for encrypting and decryption of data. Encryption is applied to thinks like in cookies, Post Id, User Id, ..etc.
What you encrpyt will be different each time. Meaning if you encrypred string "ABC", suppose you will get "xD3msr4", next time you encrypt "ABC" you will get totally different output. Definitely, either way you will always get the original string when you decrypt.
Validation is a small library for validating user inputs. All validation rules are inside Validation
Class.
$validation = new Validation();
//there are default error messages for each rule
//but, you still can define your custom error message
$validation->addRuleMessage("emailUnique", "The email you entered is already exists");
if(!$validation->validate([
"User Name" => [$name, "required|alphaNumWithSpaces|minLen(4)|maxLen(30)"],
"Email" => [$email, "required|email|emailUnique|maxLen(50)"],
'Password' => [$password,"required|equals(".$confirmPassword.")|minLen(6)|password"],
'Password Confirmation' => [$confirmPassword, 'required']])) {
var_dump($validation->errors());
}
Error
Class is responsible for handling all exceptions and errors. It will use Logger to log error. Error reporting is turned off by default, because every error will be logged and saved in app/logs/log.txt.
If error encountered or exception was thrown, the application will show System Error(500).
A place where you can log, write any failures, errors, exceptions, or any other malicious actions or attacks.
Logger::log("COOKIE", self::$userId . " is trying to login using invalid cookie", __FILE__, __LINE__);
Emails are sent using PHPMailer via SMTP, another awesome library for sending emails. You shouldn't use mail()
function of PHP.
NOTE You need to configure your SMTP account data in app/config/config.php.
Think of News Feed as tweets in twitter, and in Posts like when you open an Issue in Github.
They are implemented to be merged with the core features mentioned above. Also apply some concepts like Pagination, How can you edit & delete in place(secured way), How can you manage permissions for who can create, edit, update and delete, and so forth.
You will see each newsfeed comes with and encrypted id like: feed-51b2cfa
.
Nothing wired to explain, You can upload and download.
file_uploads
to trueupload_max_filesize, max_file_uploads, post_max_size
Every user can change his name, email, password. Also upload profile picture, initially(default.png).
Did 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_.
So, 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.
Only admins have access to see all registered users. They can delete, edit their info.
In most of the situations, you will need to create backups for the system, and restore them whenever you want.
This is done by using mysqldump to create and restore backups. All backups will be stored in app/backups.
I'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.
There 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.
If you learnt something, or I saved your time, please support the project by spreading the word.
Contribute by creating new issues, sending pull requests on Github or you can send an email at: omar.elgabry.93@gmail.com
Built under MIT license.