CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

i18n routes with CakePHP 1.3

Internationalizing a CakePHP application can be tricky when it comes to deal with i18n urls. We will see in this article how the Custom route classes introduced by CakePHP 1.3 could be used to add the current language to your urls in a few lines of code.

EDIT: This proof of concept has now been improved and a better version of the code below can be found in CakeDC's I18n plugin on Github

Requirements

This article will not go too deep in internationalizing an application as many resources already exist about it. We suppose the following:

  • Your application defines the current language on given the language code passed in the url
  • The available languages are configured via Configure::write('Config.languages', array('eng', 'fre', 'deu'));
  • You use the CakePHP array syntax for defining urls:
    • $this->Html->link('link', array('controller' => 'posts', 'action' => 'view', $post['Post']['id']));
    • $this->redirect(array('controller' => 'posts', 'action' => 'index'));
    • Router::url(array('controller' => 'posts', 'action' => 'index'), true);

Custom routes were already introduced by Mark Story on his blog, so we will not do it again here... before continuing be sure you have read "Using custom Route classes in CakePHP"

Show me some code!

I18nRoute

As I said (or not), routes are probably the best place for customizing your urls and add information in them... much more better at least than overriding the Helper::url() method in an AppHelper class!

Custom routes introduced a way to customize how routes are processed in a very easy and powerful way (i.e ~20 lines of code). It is a bit like wrapping the Router class in CakePHP 1.2, a good example of this was the CroogoRouter.

First, we are going to create an I18nRoute class extending CakeRoute in the "/libs/routes/i18n_route.php" file. Here is its code:

<?php
class I18nRoute extends CakeRoute {
/**
 * Constructor for a Route
 * Add a regex condition on the lang param to be sure it matches the available langs
 *
 * @param string $template Template string with parameter placeholders
 * @param array $defaults Array of defaults for the route.
 * @param string $params Array of parameters and additional options for the Route
 * @return void
 * @access public
 */
	public function __construct($template, $defaults = array(), $options = array()) {
		$options = array_merge((array)$options, array(
			'lang' => join('|', Configure::read('Config.languages'))
		));
		parent::__construct($template, $defaults, $options);
	}

/**
 * Attempt to match a url array.  If the url matches the route parameters + settings, then
 * return a generated string url.  If the url doesn't match the route parameters false will be returned.
 * This method handles the reverse routing or conversion of url arrays into string urls.
 *
 * @param array $url An array of parameters to check matching with.
 * @return mixed Either a string url for the parameters if they match or false.
 * @access public
 */
	public function match($url) {
		if (empty($url['lang'])) {
			$url['lang'] = Configure::read('Config.language');
		}
		return parent::match($url);
	}

}

The most important part of the code is in the "match()" method. We just add the current language to the url "lang" named param if it was not set. The constructor was also overriden to add a regex pattern for the "lang" param. Thus, only lang prefixes defined in your list of available languages will be parsed by the route.

Define your routes

It is now time to use this custom route in your application. Here is how the default route for pages could be defined in "/config/routes.php":

App::import('Lib', 'routes/I18nRoute');
Router::connect('/:lang/pages/*', array('controller' => 'pages', 'action' => 'display'), array('routeClass' => 'I18nRoute'));
  1. import the library file containing the custom route
  2. add a ":lang" param in where you want the language code appear in the url
  3. tell the Router you want to use this custom class (third param)

Link from everywhere!

Now you won't have to worry about the language code transmitted in your urls... every generated link will contain the current language code. If you want to switch the language (for instance switching to the French version of your application), you will just have to add the "lang" param to the url array.

Here are some examples of urls which would be generated on the "/eng/posts/index" page:

$this->Html->link(__('French', true), array_merge($this->passedArgs, array('lang' => 'fre'))); // /fre/posts/index
$this->Html->link('link', array('controller' => 'posts', 'action' => 'view', $post['Post']['id'])); // /eng/posts/view/2

Disclaimer

This code is experimental and the article shows you how to use CustomRoutes to implement this basic feature. Many improvements could be added to fit your needs (no language code for the default application lang, short languages code...)

Even if the tests we made were successful, we have not used this code in production yet so there may be "real word" use cases that are not handled correctly with this solution... if you find one, please tell us in the comments!

Latest articles

Building an RBAC based application in CakePHP (2/2)

This is the second article about RBAC in CakePHP series (2/2). In our previous post we did a quick introduction to RBAC and how to setup CakeDC/Auth plugin in an example project, dealing with basic array based rules. Today we'll talk about how to debug rules, and provide complex Auth rules to check permissions. We'll also discuss how to encapsulate the rules logic into `Rules` classes, and how to deal with RBAC in big projects.  

Debugging rules

Notice when debug is enabled, a detailed trace of the matched rule allowing a given action is logged into debug.log For example: 2017-10-04 23:58:10 Debug: For {"prefix":null,"plugin":null,"extension":null,"controller":"Categories","action":"index","role":"admin"} --> Rule matched {"role":"*","controller":"*","action":["index","view"],"allowed":true} with result = 1 This log could save you some time while debugging why a specific action is granted.

Callbacks for complex authentication rules

Let's imagine a more complex rule, for example, we want to block access to the articles/add action if the user has more than 3 articles already created. In this case we are going to use a callback to define at runtime the result of the allowed key in the rule. [ 'role' => '*', 'controller' => 'Articles', 'action' => 'add', 'allowed' => function (array $user, $role, \Cake\Http\ServerRequest $request) { $userId = $user['id'] ?? null; if (!$userId) { return false; } $articlesCount = \Cake\ORM\TableRegistry::get('Articles')->findByUserId($userId)->count(); return $articlesCount <= 3; } ],

Rules example

As previously discussed, we have the ability to create complex logic to check if a given role is allowed to access an action, but we could also extend this concept to define permission rules that affect specific users. One common use case is allowing the owner of the resource access to a restricted set of actions, for example the author of a given article could have access to edit and delete the entry. This case was so common that we've included a predefined Rule class you can use after minimal configuration. The final rule would be like this one: [ 'role' => '*', 'controller' => 'Articles', 'action' => ['edit', 'delete'], 'allowed' => new \CakeDC\Auth\Rbac\Rules\Owner(), ], The Owner rule will use by default the user_id field in articles table to match the logged in user id. You can customize the columns, and how the article id is extracted. This covers most of the cases where you need to identify the owner of a given row to assign specific permissions.

Other considerations

Permissions and big projects

Having permission rules in a single file could be a solution for small projects, but when they grow, it's usually hard to manage them. How could we deal with the complexity?
  • Break permission file into additional configuration files
  • Per role, usually a good idea when you have a different set of permissions per role. You can use the Configure class to append the permissions, usually having a defaults file with common permissions would be a good idea, then you can read N files, one per role to apply the specific permissions per role.
  • Per feature/plugin, useful when you have a lot of actions, and a small set of roles, or when the roles are mostly the same regarding permissions, with a couple changes between them. In this case you will define the rules in N files, each one covering a subset of the actions in your application, for example invoices.php file would add the pemissions to the Invoices plugin. In the case you work with plugins, keep in mind you could write the permission rules inside each plugin and share/distribute the rules if you reuse the plugin in other apps (as long as the other apps will have similar roles).
  • QA and maintenance
  • It's always a good idea to think about the complexity of testing the application based on the existing roles. Automated integration testing helps a lot, but if you are planning to have some real humans doing click through, each role will multiply the time to pass a full regression test on the release. Key question here is "Do we really need this role?"
  • Having a clear and documented permissions matrix file, with roles vs actions and either "YES" | "NO" | "RuleName" in the cell value will help a lot to understand if the given role should be allowed to access to a given action. If it's a CSV file it could be actually used to create a unit test and check at least the static permission rules.
  • Debugging and tracing is also important, for that reason we've included a trace feature in CakeDC/Auth that logs to debug.log the rule matched to allow/deny a specific auth check.

About performance

Performance "could" become an issue in the case you have a huge amount of rules, and some of them would require database access to check if they are matching. As a general recommendation, remember the following tips:
  • Rules are matched top to bottom
  • Try to leave the permission rules reading the database to the end of the file
  • Cache the commonly used queries, possibly the same query will be used again soon
  • Note cache invalidation is always fun, and could lead to very complex scenarios, keep it simple
  • If you need too much context and database interaction for a given rule, maybe the check should be done elsewhere. You could give some flexibility and get some performance in return

Last words

We've collected some notes about the implementation of a RBAC based system in CakePHP using our CakeDC/Auth plugin. As stated before, there are many other ways, but this is ours, worked well on several projects and we thought it was a good idea to share it with other members of the CakePHP community to expose a possible solution for their next project Authorization flavor. Please let us know if you use it, we are always improving on them - And happy to get issues and pull requests for our open source plugins. As part of our open source work in CakeDC, we maintain many open source plugins as well as contribute to the CakePHP Community. Reference

Building an RBAC based application in CakePHP (1/2)

This is the first post of a small series covering how to setup, organize and implement an RBAC based authorization system in CakePHP using the CakeDC/Auth Plugin. We'll cover the basic concepts, setup and implementation of the basic permission rules in part 1.

What does RBAC mean in this context?

We'll use RBAC as "Role Base Access Control", meaning your app will be using the following concepts to define who can access what:
  • "Who" is an existing user, mainly identified as his role in the system, such as an "admin" or "writer", etc.
  • "What" is a specific action in your application, identified as the associated routing params, for example ['controller' => 'Posts', 'action' => 'add'].
  • A permission in this context would be a link between who, and what.

Why not ACL?

ACL is a really good choice when your answer is 'yes' to any of the following questions:
  • Do we need to let users create new roles on the fly?
  • Do we need the roles to inherit permissions (tree structure)?
  • Do we need to assign permissions NOT based on controller actions? For example CRUD based permissions, checked on the model layer for each operation on a given row.
If your answer is yes, you should consider using cakephp/acl. It provides a very powerful, reliable and flexible way to configure your permissions, but with greater power comes a bigger maintenance burden, that is keeping the acl data in your tables. Specially if you have several environments to maintain, you'll need to write migrations to populate your acl tables, then create import/export scripts and utilities to reproduce permission issues from live environments, and so on. Not an impossible task, but could increase the complexity of your project in a significant way...

Setting up CakeDC/Auth

There are other plugins you could use, but this one will cover everything you'll need, so let's go. CakeDC/Auth usually comes installed from within CakeDC/Users (a complete solution covering many more features) but today we'll set it up alone. composer require cakedc/auth bin/cake plugin load CakeDC/Auth And last, but not least, add the RBAC Auth to the list of Authorize objects. Here is a working configuration based on the blog tutorial. We'll be using the blog tutorial described in the book as an example application Change AppController.php Auth related configuration to: $this->loadComponent('Auth', [ 'authorize' => ['CakeDC/Auth.SimpleRbac'], 'loginRedirect' => [ 'controller' => 'Articles', 'action' => 'index' ], 'logoutRedirect' => [ 'controller' => 'Pages', 'action' => 'display', 'home' ] ]); With this change, we'll be using only the rules defined in config/permissions.php file. If this file is not present, default permissions will be in place. Default permissions will grant access to admin role to all actions. To override permissions, you can copy the default permissions to your project and fix the rules: cp vendor/cakedc/auth/config/permissions.php config/ Then edit this file and check the provided examples and defaults.

Using CakeDC/Auth

The core of the RBAC system is the ability to define permission rules that will match one given role with the actions granted. Rules are defined in an array, but you can extend the AbstractProvider class to retrieve the rules from somewhere else (database?). By default, nothing will be granted. Rules are evaluated top to bottom. The first rule matched will stop the evaluation, and the authentication result will be provided by the value of the allowed key. Note we can use a callback to implement complex rules, or encapsulate the rules into classes that we could reuse across projects, like the Owner rule class provided. This is an example rule [ 'role' => '*', 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => ['profile', 'logout'], ], We could read this rule as follows: "For any role, we grant access to actions 'profile' and 'logout' in the Users controller in plugin CakeDC/Users". Note default allowed value is true, we can use an array, or a string to determine the role, plugin, controller and action. We can also use * in the value to match anything or use * at the start of the key to match anything but the values, for example '*controller' => 'Users', would match all the controllers but 'Users'.

Simple Rules

As our first objective, we are going to grant access to all the index and view pages, using the rule [ 'role' => '*', 'controller' => '*', 'action' => ['index', 'view'], ],
Stay tuned for the second post, where we'll deal with complex rules, rule classes and implementation tips for complex applications...

Has your website been hacked? Learn more on what you can do

If you have a website and have not made the necessary security precautions, then you may become victim to hacking.   Besides the obvious defacing that can take place once your website has fallen victim, here are some other signs you have been hacked:

  • Your website redirects to another site, not your own.
  • Google or Bing notifies you.
  • Your browser indicates that your site is not secure.
  • You notice a change in your website traffic, especially from different countries.
  So you’ve been hacked - what do you do? Where do you start?   We’ve put together a few things that you need to look into as soon as possible!  
  • Do you have a support team? Contact them!
In this situation, it is best to immediately contact your technical support - your web developers who have experience in how to handle these situations. From what to shut down, what to look for and where to check. Someone without the technical expertise to help you is going to have difficulty properly fixing things!  
  • Get together all of the information required for your support team
Your support team will need all the access information, so start putting this together - things they will need access to include your CMS; hosting provider and login details; web logs, FTP/sFTP access credentials as well as any back ups you may have. If you have never been hacked or do not have regular back ups running - here’s a good place to start.  
  • Temporarily take your website down
If you haven’t already done so, make sure to take your site down temporarily. While you are doing this, it is also important to check all your servers and computers for malware, spyware or trojans. And if you have a virtual server, it may be in the best interest to start over - some attacks leave software that may not be visible or you may not know what to look for.  
  • Change your passwords
Make sure to change your passwords! Not sure what to use? For the best security, make use of a password generator that includes both letters and numbers of more than 12.
  For expert development and consultation services, give CakeDC a call - we lead, so you can lead.

We Bake with CakePHP