CakePHP Subdomain Routing & Misc - cakephp

Background: Building a web app (as an introduction to CakePHP) which allows users to manage a lounge. A lounge is composed of a blog, contacts, calendar, etc. Each lounge is associated with a subdomain (so jcotton.lounger.local would take you to my lounge). The root of the site, used for creating new lounges, registering users, etc is hosted on lounger.local. I am using Cake 2.0.
Questions:
I wanted to be able to separate actions and views associated with the root site (lounger.local) from individual lounges (subdomains of lounger.local). After a good deal of research I settled on the following soln. I setup a prefix route "lounge" and added the the following code in routes.php. Actions (and views) associated with a lounge all contain the prefix lounge (ex: lounge_index()). How would you handle this?
if(preg_match('/^([^.]+)\.lounger\.local$/',env("HTTP_HOST"),$matches)){
$prefix = "lounge";
Router::connect('/', array('controller' => 'loungememberships','action' => 'index', 'prefix' => $prefix, $prefix => true));
/* Not currently using plugins
Router::connect("/:plugin/:controller", array('action' => 'index', 'prefix' => $prefix, $prefix => true));
Router::connect("/:plugin/:controller/:action/*", array('prefix' => $prefix, $prefix => true));
*/
Router::connect("/:controller", array('action' => 'index', 'prefix' => $prefix, $prefix => true));
Router::connect("/:controller/:action/*", array('prefix' => $prefix, $prefix => true));
unset($prefix);
}
Each time a user performs an action within a lounge such as posting a comment within the blog, adding a contact, etc, it is necessary to lookup the lounge_id (based on the subdomain); this is necessary to verify the user is authorized to perform that action and to associate the corresponding data with the correct lounge. I have implemented this via the beforeFilter function in AppController. Each time a request is received with a subdomain a search is performed and the lounge_id is written to a session variable. Each controller then loads CakeSession and reads the corresponding lounge_id. Is this better than calling ClassRegistry::Init('Lounge') and doing the lookup in each controller? Is there a better soln?
Thanks in advance for the help

The way I approached this was with a custom route, and some trickery with route configuration similar to your example.
First, I have a "Master domain" that is redirected to and used as the main domain for the multi-tenancy site. I also store a default action i want them to take. I store these in configuration variables:
Configure::write('Domain.Master', 'mastersite.local');
Configure::write('Domain.DefaultRoute', array('controller' => 'sites', 'action' => 'add'));
Next, I created a DomainRoute route class in /Lib/Route/DomainRoute.php:
<?php
App::uses('CakeRoute', 'Routing/Route');
App::uses('CakeResponse', 'Network');
App::uses('Cause', 'Model');
/**
* Domain Route class will ensure a domain has been setup before allowing
* users to continue on routes for that domain. Instead, it redirects them
* to a default route if the domain name is not in the system, allowing
* creation of accounts, or whatever.
*
* #package default
* #author Graham Weldon (http://grahamweldon.com)
*/
class DomainRoute extends CakeRoute {
/**
* A CakeResponse object
*
* #var CakeResponse
*/
public $response = null;
/**
* Flag for disabling exit() when this route parses a url.
*
* #var boolean
*/
public $stop = true;
/**
* Parses a string url into an array. Parsed urls will result in an automatic
* redirection
*
* #param string $url The url to parse
* #return boolean False on failure
*/
public function parse($url) {
$params = parent::parse($url);
if ($params === false) {
return false;
}
$domain = env('HTTP_HOST');
$masterDomain = Configure::read('Domain.Master');
if ($domain !== $masterDomain) {
$defaultRoute = Configure::read('Domain.DefaultRoute');
$Cause = new Cause();
if (!($Cause->domainExists($domain)) && $params != $defaultRoute) {
if (!$this->response) {
$this->response = new CakeResponse();
}
$status = 307;
$redirect = $defaultRoute;
$this->response->header(array('Location' => Router::url($redirect, true)));
$this->response->statusCode($status);
$this->response->send();
$this->_stop();
}
$params['domain'] = $domain;
}
return $params;
}
/**
* Stop execution of the current script. Wraps exit() making
* testing easier.
*
* #param integer|string $status see http://php.net/exit for values
* #return void
*/
protected function _stop($code = 0) {
if ($this->stop) {
exit($code);
}
}
}
This custom route class is used within the /Config/routes.php file to setup multi-tenancy.
if (env('HTTP_HOST') === Configure::read('Domain.Master')) {
// Master domain shows the home page.
$rootRoute = array('controller' => 'pages', 'action' => 'display', 'home');
} else {
// Subdomains show the cause view page.
$rootRoute = array('controller' => 'causes', 'action' => 'view', env('HTTP_HOST'));
}
Router::connect('/', $rootRoute, array('routeClass' => 'DomainRoute'));
On inspection of the custom router, you will see that I am pulling the current domain being accessed and adding that to the $params array.
While this does not directly achieve what you are after, minor modifications will get you on the right track with your requirements. There is not a great deal of information about Custom Routes, but here is the CakePHP documentation link for custom route classes.
I hope that helps!

Related

What do I do when I want to separate models with authentication plugins?

I'm currently implementing the login feature in CakePHP4.
So I wanted to divide it into a normal user model and an administrative user model.
I didn't know how to do this at all, and I spent all day looking for ways to implement it, but I couldn't do it.
I want to set up authentication in "routes.php", but I'm having trouble with "implementing AuthenticationServiceProviderInterface" in "Application.php". What should I do?
// routes.php
$routes->scope('/', function (RouteBuilder $builder) {
/......./
// ログイン
//$builder->connect('mgt-account/*', ['controller' => 'MgtAccount', 'action' => 'login', 'prefix' => 'Admin']);
$builder->connect('users/*', ['controller' => 'Users', 'action' => 'login', 'prefix' => 'Normal']);
/......./
});
$routes->prefix('Normal', function (RouteBuilder $routes) {
$loginUrl = Router::url('/normal/users/login');
$fields = [
'username' => 'mail',
'password' => 'password',
];
$service = new AuthenticationService([
'unauthenticatedRedirect' => $loginUrl,
'queryParam' => 'redirect',
]);
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('Authentication.Form', [
'fields' => $fields,
'loginUrl' => $loginUrl,
]);
$service->loadIdentifier('Authentication.Password', compact('fields'));
$routes->registerMiddleware(
'auth',
new \Authentication\Middleware\AuthenticationMiddleware($service)
);
$routes->applyMiddleware('auth');
//$routes->connect('/:controller');
$routes->fallbacks(DashedRoute::class);
});
$routes->prefix('Admin', function (RouteBuilder $routes) {
$loginUrl = Router::url('/admin/mgt-account/login');
$fields = [
'username' => 'mail',
'password' => 'password',
];
$service = new AuthenticationService([
'unauthenticatedRedirect' => $loginUrl,
'queryParam' => 'redirect',
]);
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('Authentication.Form', [
'fields' => $fields,
'loginUrl' => $loginUrl,
]);
$service->loadIdentifier('Authentication.Password', compact('fields'));
$routes->registerMiddleware(
'auth',
new \Authentication\Middleware\AuthenticationMiddleware($service)
);
$routes->applyMiddleware('auth');
//$routes->connect('/:controller');
$routes->fallbacks(DashedRoute::class);
});
<?php
// src/Application.php
declare(strict_types=1);
namespace App;
use Cake\Core\Configure;
use Cake\Core\Exception\MissingPluginException;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
use Cake\Http\BaseApplication;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;
/**
* Application setup class.
*
* This defines the bootstrapping logic and middleware layers you
* want to use in your application.
*/
class Application extends BaseApplication
// I really want to comment out and delete this bottom.
implements AuthenticationServiceProviderInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* #return void
*/
public function bootstrap(): void
{
// Call parent to load bootstrap from files.
parent::bootstrap();
if (PHP_SAPI === 'cli') {
$this->bootstrapCli();
}
/*
* Only try to load DebugKit in development mode
* Debug Kit should not be installed on a production system
*/
if (Configure::read('debug')) {
$this->addPlugin('DebugKit');
}
// Load more plugins here
}
/**
* Setup the middleware queue your application will use.
*
* #param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup.
* #return \Cake\Http\MiddlewareQueue The updated middleware queue.
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
// Catch any exceptions in the lower layers,
// and make an error page/response
->add(new ErrorHandlerMiddleware(Configure::read('Error')))
// Handle plugin/theme assets like CakePHP normally does.
->add(new AssetMiddleware([
'cacheTime' => Configure::read('Asset.cacheTime'),
]))
// Add routing middleware.
// If you have a large number of routes connected, turning on routes
// caching in production could improve performance. For that when
// creating the middleware instance specify the cache config name by
// using it's second constructor argument:
// `new RoutingMiddleware($this, '_cake_routes_')`
->add(new RoutingMiddleware($this))
// I really want to comment out and delete this bottom.
->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}
/**
* Bootrapping for CLI application.
*
* That is when running commands.
*
* #return void
*/
protected function bootstrapCli(): void
{
try {
$this->addPlugin('Bake');
} catch (MissingPluginException $e) {
// Do not halt if the plugin is missing
}
$this->addPlugin('Migrations');
// Load more plugins here
}
// I really want to comment out and delete this bottom.
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$authenticationService = new AuthenticationService([
'unauthenticatedRedirect' => '/normal/users/login',
'queryParam' => 'redirect',
]);
$authenticationService->loadIdentifier('Authentication.Password', [
'fields' => [
'username' => 'mail',
'password' => 'password',
]
]);
$authenticationService->loadAuthenticator('Authentication.Session');
$authenticationService->loadAuthenticator('Authentication.Form', [
'fields' => [
'username' => 'mail',
'password' => 'password',
],
'loginUrl' => '/normal/users/login',
]);
return $authenticationService;
}
}
As this is my first time using "stackoverflow", I'm not sure how to ask a good question. I'd appreciate it if you could help me out.
I would appreciate it if you could point it out to me.
thank you.
I believe the answer is to load the correct middleware authenticator based on the current request (admin or regular user). But I have not multiple authentications yet so might not be correct. In Application.php method getAuthenticationService(), where you call loadIdentifier(), specify one "resolver" or the other depending on the current request URL (or however you distinguish an admin url).
The CakePHP 4.x documentation has a section on multiple authentication schemes. I believe you can use two different tables. Configuring Multiple Authentication Setups
This forum item may have the answer you need (the answer below the question): Multiple Authentication Setups

Laravel 5 Password Reset with Angular View

I am trying to use the Laravel inbuilt password reset in my app where Laravel 5.1 acts as the backend api and Angular 1.3 for all front-end views. I have set-up the Password reset as per the docs where I have done the following:
1) Create the table
php artisan migrate
2) Added this to the route:
Route::post('password/email', 'Auth/PasswordController#postEmail');
Route::post('password/reset', 'Auth/PasswordController#postReset');
Since I will be using Angular to display frontend forms, I did not add the views for GET. I havent done any changes to the Auth/PasswordController.php and right now its just like the way it came. But when I test the above URL from Postman POST request, I am getting the error:
View [emails.password] not found.
How can I let Angular Handle the views and not have Laravel worry about the view? Do I have to have Laravel View for the inbuilt password reset to work? How do I approach this?
Override the postEmail and postReset methods so that they return a JSON response (don't let it redirect). Subsequently post to /password/email and /password/reset from Angular via xhr.
Open app/Http/Controllers/Auth/PasswordController.php
<?php namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
class PasswordController extends Controller
{
use ResetsPasswords;
//add and modify this methods as you wish:
/**
* Send a reset link to the given user.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function postEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
$response = Password::sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
});
switch ($response) {
case Password::RESET_LINK_SENT:
return redirect()->back()->with('status', trans($response));
case Password::INVALID_USER:
return redirect()->back()->withErrors(['email' => trans($response)]);
}
}
/**
* Reset the given user's password.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function postReset(Request $request)
{
$this->validate($request, [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed',
]);
$credentials = $request->only(
'email', 'password', 'password_confirmation', 'token'
);
$response = Password::reset($credentials, function ($user, $password) {
$this->resetPassword($user, $password);
});
switch ($response) {
case Password::PASSWORD_RESET:
return redirect($this->redirectPath());
default:
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
}
}
Ceckout your path to views folder in app\bootstrap\cache\config.php at section "view"
'view' =>
array (
'paths' =>
array (
0 => '/home/vagrant/Code/app/resources/views',
),
'compiled' => '/home/vagrant/Code/app/storage/framework/views',
),
this path MUST be at SERVER! not at you local mashine like
"D:\WebServers\home\Laravel\app\bootstrap\cache", if you use the homestead.
And You must use command like: "php artisan config:clear | cache" at SERVER!
I had the same problem than you. You could manage to change the view in config/auth.php if you have another one not in resources/views/emails/password.blade.php.
Because this view isn't created by default, that's why you got the error.

Plugin route to a prefixed controller

I'm creating a plugin for my application (using CakePHP 2.6.0) that allows users to login into a user area using the same model as for the admin area, so I'm trying to get the same type of URI scheme as the admin area e.g. /admin/users/login but then for /special/users/login. I have the following route in my plugin's Config/routes.php and added the 'special' prefix to the 'Routing.prefixes' configuration:
Router::connect('/special/:controller/:action', array(
'special' => true,
'prefix' => 'special',
'plugin' => 'Special',
'controller' => ':controller',
'action' => ':action',
));
With above route and entering the following url /special/users/login it would make sense to me right now if Cake went for Plugin/Controller/SpecialUsersController.php (considering namespace conflicts with the main application's UsersController) but instead I get an error Error: Create the class UsersController.
Is there a built-in way for it to load a prefixed controller (without changing the url) based on the plugin? Or is there a better way to neatly extend my main application? Am I going about this the wrong way?
I could not find a built-in way for it to work as I wanted to so I changed the way URL strings were parsed using a custom route class in my app's Routing/Route/ folder:
App::uses('CakeRoute', 'Routing/Route');
/**
* Plugin route will make sure plugin routes get
* redirected to a prefixed controller.
*/
class PluginRoute extends CakeRoute {
/**
* Parses a string URL into an array.
*
* #param string $url The URL to parse
* #return bool False on failure
*/
public function parse($url) {
$params = parent::parse($url);
if($params && !empty($params['controller']) && !empty($params['plugin'])) {
$params['controller'] = $params['plugin'] . ucfirst($params['controller']);
}
return $params;
}
}
And then setting up my plugin routes as:
App::uses('PluginRoute', 'Routing/Route');
Router::connect('/special/:controller/:action', array(
'special' => true,
'prefix' => 'special',
'plugin' => 'Special',
'controller' => ':controller',
'action' => ':action',
), array(
'routeClass' => 'PluginRoute'
));
This resulted in /special/users/login creating the controller I wanted Plugin/Controller/SpecialUsersController.php, no side effects so far. Extending the main application's UsersController is a different story though.
Maybe someone else has use of this or knows a better solution?

Add a subdomain to all urls in Cakephp

I want all Cakephp urls to use a subdomain if it is present in the users session... so if there is an entry 'subdomain:user' in the session of the user all pages will have 'user' as a prefix: Eg user.example.com, user.example.com/settings.
Is there an easy way to do this?
thanks,
kSeudo
There are custom route classes you could use. Create a file in APP/Lib/Route called UserSubdomainRoute.php and put this in it.
<?php
App::uses('AuthComponent', 'Controller/Component');
class UserSubdomainRoute extends CakeRoute {
/**
* #var CakeRequest
*/
private $Request;
/**
* Length of domain's TLD
*
* #var int
*/
public static $_tldLength = 1;
public function __construct($template, $defaults = array(), $options = array()) {
parent::__construct($template, $defaults, $options);
$this->Request = new CakeRequest();
$this->options = array_merge(array('protocol' => 'http'), $options);
}
/**
* Sets tld length
*
* #param $length
* #return mixed void|int
*/
public static function tldLength($length = null) {
if (is_null($length)) {
return self::$_tldLength;
}
self::$_tldLength = $length;
}
/**
* Writes out full url with protocol and subdomain
*
* #param $params
* #return string
*/
protected function _writeUrl($params) {
$protocol = $this->options['protocol'];
$subdomain = AuthComponent::user('subdomain');
if (empty($subdomain)) {
$subdomain = 'www';
}
$domain = $this->_getDomain();
$url = parent::_writeUrl($params);
return "{$protocol}://{$subdomain}.{$domain}{$url}";
}
/**
* Get domain name
*
* #return string
*/
protected function _getDomain() {
return $this->Request->domain(self::$_tldLength);
}
}
One improvement to the class would probably be to make the $Request static.
Unfortunately in Cake 2.0 there is no way to set a defaultRotueClass, however, I added that feature in 2.1+ and I don't want to tell you to upgrade so you are going to have to manually specify it for all your routes in the third param like so:
Router::connect(..., ..., array('routeClass' => 'UserSubdomainRoute');
Be sure to add at the top of routes.php
App::uses('UserSubdomainRoute', 'Lib/Route');
If you do upgrade to 2.1+ you can just add this at the top of your routes.php
Router::defaultRouteClass('UserSubdomainRoute');
Then any routs specified after will use that route class.
The main part of the route class is the _writeUrl method. It checks to see if there is a subdomain key set in the session otherwise uses www and builds the full url to return.
Heads Up: Haven't tested the class, still at school just wanted to give you a jump start. It's just a modifed version of my SubdomainRoute which works a bit differently (it used to only match routes to when a certain subdomain is set, for ex in my app matches clients subdomain to my ClientsAdminPanel plugin. You can grab that here: http://bin.cakephp.org/view/50699397 So you can see how that's done as well if you need a combination of UserSubdomainRoute and my SubdomainRoute (in the link).
Hope this helps for now. Let me know if there are any problems.
Edit:
Here's how to force a redirection - something tells me there's a better way. I'll update if I can think of it.
public function beforeFilter() {
$subdomains = $this->request->subdomains();
$subdomain = $this->Auth->user('subdomain');
if (!empty($subdomain) && !in_array($subdomain, $subdomains)) {
$this->redirect('http://' . $subdomain . '.site.com' . $this->request->here);
}
}
well if you wish to do it for all your links probably you use the same way to show all your url on your site, do you use something like $this->html->link('/blah/asd/','blah'); ?
IN Cake/Routing/Route
class SubdomainRoute extends CakeRoute {
public function match($params) {
$subdomain = isset($params['subdomain']) ? $params['subdomain'] : null;
unset($params['subdomain']);
$path = parent::match($params);
if ($subdomain) {
$path = 'http://' . $subdomain . '.localhost' . $path;
}
return $path;
}
}
When creating links you could do the following to make links pointing at other subdomains.
echo $this->Html->link(
'Other domain',
array('subdomain' => 'test', 'controller' => 'posts', 'action' => 'add')
);

Adding a prefix to every URL in CakePHP

What's the cleanest way to add a prefix to every URL in CakePHP, like a language parameter?
http://example.com/en/controller/action
http://example.com/ru/admin/controller/action
It needs to work with "real" prefixes like admin, and ideally the bare URL /controller/action could be redirected to /DEFAULT-LANGUAGE/controller/action.
It's working in a retro-fitted application for me now, but it was kind of a hack, and I need to include the language parameter by hand in most links, which is not good.
So the question is twofold:
What's the best way to structure Routes, so the language parameter is implicitly included by default without having to be specified for each newly defined Route?
Router::connect('/:controller/:action/*', ...) should implicitly include the prefix.
The parameter should be available in $this->params['lang'] or somewhere similar to be evaluated in AppController::beforeFilter().
How to get Router::url() to automatically include the prefix in the URL, if not explicitly specified?
Router::url(array('controller' => 'foo', 'action' => 'bar')) should return /en/foo/bar
Since Controller::redirect(), Form::create() or even Router::url() directly need to have the same behavior, overriding every single function is not really an option. Html::image() for instance should produce a prefix-less URL though.
The following methods seem to call Router::url.
Controller::redirect
Controller::flash
Dispatcher::__extractParams via Object::requestAction
Helper::url
JsHelper::load_
JsHelper::redirect_
View::uuid, but only for a hash generation
Out of those it seems the Controller and Helper methods would need to be overridden, I could live without the JsHelper. My idea would be to write a general function in AppController or maybe just in bootstrap.php to handle the parameter insertion. The overridden Controller and Helper methods would use this function, as would I if I wanted to manually call Router::url. Would this be sufficient?
This is essentially all the code I implemented to solve this problem in the end (at least I think that's all ;-)):
/config/bootstrap.php
define('DEFAULT_LANGUAGE', 'jpn');
if (!function_exists('router_url_language')) {
function router_url_language($url) {
if ($lang = Configure::read('Config.language')) {
if (is_array($url)) {
if (!isset($url['language'])) {
$url['language'] = $lang;
}
if ($url['language'] == DEFAULT_LANGUAGE) {
unset($url['language']);
}
} else if ($url == '/' && $lang !== DEFAULT_LANGUAGE) {
$url.= $lang;
}
}
return $url;
}
}
/config/core.php
Configure::write('Config.language', 'jpn');
/app_helper.php
class AppHelper extends Helper {
public function url($url = null, $full = false) {
return parent::url(router_url_language($url), $full);
}
}
/app_controller.php
class AppController extends Controller {
public function beforeFilter() {
if (isset($this->params['language'])) {
Configure::write('Config.language', $this->params['language']);
}
}
public function redirect($url, $status = null, $exit = true) {
parent::redirect(router_url_language($url), $status, $exit);
}
public function flash($message, $url, $pause = 1) {
parent::flash($message, router_url_language($url), $pause);
}
}
/config/routes.php
Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display'));
Router::connect('/:language/', array('controller' => 'pages', 'action' => 'display', 'home'), array('language' => '[a-z]{3}'));
Router::connect('/:language/pages/*', array('controller' => 'pages', 'action' => 'display'), array('language' => '[a-z]{3}'));
Router::connect('/:language/:controller/:action/*', array(), array('language' => '[a-z]{3}'));
This allows default URLs like /controller/action to use the default language (JPN in my case), and URLs like /eng/controller/action to use an alternative language. This logic can be changed pretty easily in the router_url_language() function.
For this to work I also need to define two routes for each route, one containing the /:language/ parameter and one without. At least I couldn't figure out how to do it another way.
rchavik from IRC suggested this link: CakePHP URL based language switching for i18n and l10n internationalization and localization
In general, it seems that overriding Helper::url might be the solution.
An easier way might be to store the chosen language in a cookie and then not have to rewrite all the URLs. You could also potentially detect the user's browser language automatically.
However, search engines would be unlikely to pickup the various languages and you'd also lose the language if someone tried to share the link.
But love the full solution you posted, very comprehensive, thanks. :-)

Resources