CakePHP4: Where should I put my custom PasswordHasher - php

I implemented CakePHP4 authentication following this:
https://book.cakephp.org/4/en/tutorials-and-examples/cms/authentication.html
It worked, then I need to use my custom PasswordHasher to satisfy the client requirements. I couldn't find any tutorial to do that, but figured out the following code works.
In Application.php:
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface {
// ....
$authenticationService->loadIdentifier('Authentication.Password', [
'fields' => [
'username' => 'username',
'password' => 'password',
],
'passwordHasher' => [
'className' => 'Authentication.MyCustom',
]
]);
The problem is that I need to put MyCustomPasswordHasher.php file in vendor\cakephp\authentication\src\PasswordHasher in order to make this work. Of course I don't want to put my own code under vendor directory.
Temporarily, I created and used src\Authentication\PasswordHasher directory and forced to make it work by doing this:
spl_autoload_register(function ($class) {
if (strpos($class, 'MyCustomPasswordHasher') !== false) {
require_once __DIR__ . '/' . str_replace(['\\', 'App/'], [DS, ''], $class) . '.php';
}
});
Is there any cleaner way to accomplish the purpose in CakePHP4? Where should I put custom codes?

Don't use plugin notation for the short name, pass only 'MyCustom', then it will look inside of your application, in the App\PasswordHasher namespace, so your class would accordingly go into
src/PasswordHasher/MyCustomPasswordHasher.php
Alternatively you can always pass a fully qualified name, meaning you could put your class wherever you want, as long as the composer autoloader can resolve it:
'passwordHasher' => [
'className' => \Some\Custom\Namespaced\PasswordHasher::class,
]

Related

In Laravel can I set a default context for the Log facade

I'm using the Log:: facade a lot and have a helper class called LogHelper which provide me with a static method LogHelper::context() which include many key values I need to track the requests. But having to type it every time for each usage make it error prune and fill not so efficient.
I'm looking for a way to inject the values by default, and allow me to overwrite them if needed specifically.
At the moment this is how I use it,
Log::debug('Request Started', LogHelper::context());
what I'm looking for is to inject the context by default
Log::debug('Request Started');
and have the option to overwrite it, if need it:
Log::debug('Request Started', ['more' => 'context'] + LogHelper::context());
PS, the LogHelper::context() return a simple key => value array which include some staff i need to debug requests, and the reason it do not use the values directly in the message is because i log to graylog as structured data, and this way i can filter by any key.
I have solved this issue by using the tap functionality and $logger->withContext() (note: the latter was added in Laravel 8.49).
You want to create a new class which contains your context logic. I've created an extra Logging folder in app/ in which my logging customizations sit.
app/Logging/WithAuthContext.php:
<?php
namespace App\Logging;
use Illuminate\Log\Logger;
class WithAuthContext
{
public function __invoke(Logger $logger)
{
$logger->withContext([
'ip' => request()?->ip(),
'ua' => request()?->userAgent(),
]);
}
}
Depending on which logging channel(s) you use, you will have to add the class to each one you want to add context to. So in app/config/logging.php:
<?php
use App\Logging\WithAuthContext;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
// ...
'channels' => [
// ...
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'tap' => [WithAuthContext::class],
],
// ...
],
];
There is a way, but it is not pretty. You can create a custom monolog logger driver. The process is described at https://laravel.com/docs/8.x/logging#creating-monolog-handler-channels.
Here's a possible implementation:
class ContextEnrichingLogger extends \Monolog\Handler\AbstractHandler {
private $logger;
public function __construct($level = Monolog\Logger::DEBUG, bool $bubble = true, $underlyingLogger = 'single') {
$this->logger = Log::driver($underlyingLogger);
}
public function handle(array $record) {
$record['context'] += LogHelper::context();
return $this->logger->handle($record);
}
}
Then register this as a custom logger in your config/logging.php:
return [
'default' => 'enriched',
//...
'channels' => [
// ...
'enriched' => [
'driver' => 'monolog',
'handler' => ContextEnrichingLogger::class,
'level' => env('APP_LOG_LEVEL', 'debug'),
"with" => [
"underlyingLogger" => env('LOG_CHANNEL', 'single')
]
]
]
];
I haven't tested this particular one but this is how I've defined other custom loggers.
Note, this is probably also achievable via a custom formatter though I think it's probably the same trouble.

Custom (dynamic) log file names with laravel5.6

With laravel 5.5 we had access to configureMonologUsing() method in $app which made it possible for things like this in bootstrap/app.php:
$app->configureMonologUsing(function (Monolog\Logger $monolog) {
$processUser = posix_getpwuid(posix_geteuid());
$processName= $processUser['name'];
$filename = storage_path('logs/laravel-' . php_sapi_name() . '-' . $processName . '.log');
$handler = new Monolog\Handler\RotatingFileHandler($filename);
$monolog->pushHandler($handler);
});
Doing this is useful when your app may be called from different contexts (eg CLI/HTTP) with different users (which is desirable) and file rotation. Doing this prevents write errors in case the log file was created by the HTTP user before the CLI one tries to add something in it and vice-versa.
Handling this is otherwise tricky or insecure as it involves to be able to set write perms on files that may not exist yet.
In addition, it's quite handy to have logs separated by contexts as they usually have little in common and it makes it easier to search among them.
Unfortunately, this way of doing things is not possible anymore with laravel 5.6 and I could not (yet) find a way to transparently do so for all file based logging.
Thanks
Customisation is now done through invoking a custom formatter for Monolog.
Here is an example using daily rotating filenames (as I do).
This can be setup in config/logging.php, note the non-default tap parameter:
'channels' => [
'daily' => [
'driver' => 'daily',
'tap' => [App\Logging\CustomFilenames::class],
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
],
]
In your custom formatter, you can manipulate the Monolog logger however you wish, similar to configureMonologUsing():
app\Logging\CustomFilenames.php
<?php
namespace App\Logging;
use Monolog\Handler\RotatingFileHandler;
class CustomFilenames
{
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
if ($handler instanceof RotatingFileHandler) {
$sapi = php_sapi_name();
$handler->setFilenameFormat("{filename}-$sapi-{date}", 'Y-m-d');
}
}
}
}
One way to restore your original behaviour is to remove the {date} component from the handler's filenameFormat. A better way might be to manipulate the appropriate handler for the single driver.
See: https://laravel.com/docs/5.6/logging#advanced-monolog-channel-customization
Solution:
step1: create a channel inside the config/logging.php file
example :
'channels' => [
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
],
'web' => [
'driver' => 'single',
'path' => storage_path('logs/web/web.log'),
],
]
Step2: Now set dyanamic path from controller like this
config(['logging.channels.web.path' => storage_path('logs/web/'.time().'.log')]);
Step3 : now generate your log
Log::channel('web')->info("your message goes here");
Enjoy :)

Catch user defined URLs in UrlManager

I have the ability for users to define URLs for some of their items so, for example:
http://x.com/mynewobject
mynewobject would be defined by the user in a form and I need to be able to say in the UrlManager to math that, but also match everything else.
Problem is the default rules in the UrlManager will try and catch the mynewobject controller and throw a 404 when it cannot.
What is the way to make a UrlManager catch user defined URLs?
The best way I have found of doing this, without manually declaring your URLs, is to actually take a closer look at the Yii 2 documentation.
It actually shows a good example here of user generated URLs http://www.yiiframework.com/doc-2.0/guide-runtime-routing.html#creating-rules which I used to complete my task.
The configuration I used was:
'urlManagerFrontend' => [
'class' => 'yii\web\UrlManager',
'enablePrettyUrl' => true,
'showScriptName' => false,
'cache' => null,
'baseUrl' => '/',
'rules' => [
[
'class' => 'common\components\ObjectUrlRule',
'pattern' => '<slug:.*>',
'route' => 'site/index'
],
'<controller:[\w-]+>/<id:\d+>'=>'<controller>/view',
'<controller:[\w-]+>/<action:[\w-]+>/<id:\d+>'=>'<controller>/<action>',
'<controller:[\w-]+>/<action:[\w-]+>'=>'<controller>/<action>',
// your rules go here
]
],
And a rule class of:
<?php
namespace common\components;
use Yii;
use yii\web\UrlRule;
use common\models\ObjectUrl;
class ObjectUrlRule extends UrlRule
{
public function parseRequest($manager, $request)
{
$pathInfo = trim($request->getPathInfo());
if(!$pathInfo){
return false;
}
$controller = Yii::$app->createController($pathInfo);
if($controller){
return false;
}
$objectUrl = ObjectUrl::find()->where(['url' => $pathInfo])->one();
if(!$objectUrl){
return false;
}
return $objectUrl->getUrl($pathInfo);
}
}
Where ObjectUrl is the model (table) which contains the map of user generated URLs.
The good thing about this class, as well, is that it will not run the database call unless the controller does not exist and there is a pathinfo.

Laravel rename routing resource path names

Can we rename routing resource path names in Laravel like in Ruby on Rails?
Current
/users/create -> UsersController#create
/users/3/edit -> UsersController#edit
..
I want like this;
/users/yeni -> UsersController#create
/users/3/duzenle -> UsersController#edit
I want to do this for localization.
Example from Ruby on Rails;
scope(path_names: { new: "ekle" }) do
resources :users
end
I know this is an old question. I'm just posting this answer for historical purposes:
Laravel now has the possibility to localize the resources. https://laravel.com/docs/5.5/controllers#restful-localizing-resource-uris
Localizing Resource URIs By default, Route::resource will create
resource URIs using English verbs. If you need to localize the create
and edit action verbs, you may use the Route::resourceVerbs method.
This may be done in the boot method of your AppServiceProvider:
use Illuminate\Support\Facades\Route;
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot() {
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]); }
Once the verbs have been customized, a resource route registration such as Route::resource('fotos', 'PhotoController') will
produce the following URIs:
/fotos/crear
/fotos/{foto}/editar
It ain't pretty, but you could define multiple routes that use the same controller function. For example:
Route::get("user/create", "UsersController#create");
Route::get("user/yeni", "UsersController#create");
The only (glaringly obvious downside) being that you're routes will get quite cluttered quite quickly. There is a setting in app/config/app.php where you can set/change your locale, and it could be possible to use that in conjunction with a filter to use the routes and then group those routes based on the current local/language, but that would require more research.
As far as I know, there isn't a way to rename resource routes on the fly, but if you get creative you can figure something out. Best of luck!
You can't change the resource url's.
For this you will need to define/create each route according your needs
Route::get("user/yeni", "UsersController#create");
and if you need more than one languages you can use the trans helper function, which is an alias for the Lang::get method
Route::get('user/'.trans('routes.create'), 'UsersController#create');
I just had the same issue. And managed to recreate some sort of custom resource route method. It probably could be a lot better, but for now it works like a charm.
namespace App\Helpers;
use Illuminate\Support\Facades\App;
class RouteHelper
{
public static function NamedResourceRoute($route, $controller, $named, $except = array())
{
$routes = RouteHelper::GetDefaultResourceRoutes($route);
foreach($routes as $method => $options) {
RouteHelper::GetRoute($route, $controller, $method, $options['type'], $options['name'], $named);
}
}
public static function GetRoute($route, $controller, $method, $type, $name, $named) {
App::make('router')->$type($named.'/'.$name, ['as' => $route.'.'.$method, 'uses' => $controller.'#'.$method]);
}
public static function GetDefaultResourceRoutes($route) {
return [
'store' => [
'type' => 'post',
'name' => ''
],
'index' => [
'type' => 'get',
'name' => ''
],
'create' => [
'type' => 'get',
'name' => trans('routes.create')
],
'update' => [
'type' => 'put',
'name' => '{'.$route.'}'
],
'show' => [
'type' => 'get',
'name' => '{'.$route.'}'
],
'destroy' => [
'type' => 'delete',
'name' => '{'.$route.'}'
],
'edit' => [
'type' => 'get',
'name' => '{'.$route.'}/'.trans('routes.edit')
]
];
}
}
Use it like this in the routes.php:
\App\Helpers\RouteHelper::NamedResourceRoute('recipes', 'RecipeController', 'recepten');
Where the first parameter is for the named route, second the controller and third the route itself.
And something like this to the view/lang/{language}/route.php file:
'edit' => 'wijzig',
'create' => 'nieuw'
This results in something like this:
This is not possible in Laravel as they use code by convention over configuration. A resources uses the RESTfull implementation
Therefore you have to stick to the convention of
GET /news/create
POST /news
GET /news/1
GET /news/1/edit
...

i18n and modules retrieves from wrong path in Yii2

I don't know if this is a bug or a mistake on my end but basically I followed the Yii2 documentation to setup i18n translations for modules. The following snippet is directly copy pasted from the Yii2 guide.
public function init()
{
parent::init();
$this->registerTranslations();
}
public function registerTranslations()
{
Yii::$app->i18n->translations['modules/users/*'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en',
'basePath' => '#app/modules/users/messages',
];
}
public static function t($category, $message, $params = [], $language = null)
{
return Yii::t('modules/users/' . $category, $message, $params, $language);
}
According to the guide I should call it like this:
Module::t('validation', 'your custom validation message')
However, Yii2 tries to load the the 'validation.php' from the wrong location. This is the output of the debugger:
The message file for category 'modules/users/validation' does not exist: /Applications/MAMP/htdocs/.../domains/localhost/public_html/.../backend/modules/users/messages/en/modules/users/validation.php
From what I understand, it should be looking for modules/users/message/<lang>/validation.php instead, which makes a lot more sense than what it is looking for right now.
What am I doing wrong?
Thank you in advance.
You should simply add a filemap param, e.g. :
public function registerTranslations()
{
Yii::$app->i18n->translations['modules/users/*'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en',
'basePath' => '#app/modules/users/messages',
'fileMap' => [
'modules/users/validation' => 'validation.php',
],
];
}
Read more : http://www.yiiframework.com/doc-2.0/guide-tutorial-i18n.html#translating-module-messages
EDIT : As stated in Yii2 guide, if you want to remove filemap, your validation.php file should be in modules/users/messages/[lang]/modules/users/validation.php.
Instead of configuring fileMap you can rely on convention which is to use the category name as the file name (e.g. category app/error will result in the file name app/error.php under the basePath.

Categories