TL;DR How can I use my own way of generating the remember_me token?
I have an old site, written without any framework, and I have been given the job to rewrite it in Laravel (5.4.23). The DB is untouchable, cannot be refactored, cannot be modified in any way.
I was able to customise the Laravel authentication process using a different User model, one that reflect the old DB. But when it comes to the "Remember me" functionality, I have an issue with the length of the token.
The old site already uses the "Remember me" functionality but its DB field has been defined as BINARY(25). The token generated by the SessionGuard class is 60 characters long.
My first attempt was to try and find a way to shorten the token before writing it into the DB, and expand it again after reading it from the DB. I couldn't find such a way (and I'm not even sure there is such a way).
Then I looked into writing my own guard to override the cycleRememberToken (where the token is generated). I couldn't make it work, I think because the SessionGuard class is actually instantiated in a couple of places (as opposed to instantiate a class based on configuration).
So, I am stuck. I need a shorten token and I don't know how to get it.
Well, I was on the right track at one point.
I had to create my own guard, register it and use it. My problem, when I tried the first time, was that I did not register it in the right way. Anyway, this is what I did.
I put the following in AuthServiceProvides
Auth::extend('mysession', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
$guard = new MyGuard('lrb', $provider, app()->make('session.store'));
$guard->setCookieJar($this->app['cookie']);
$guard->setDispatcher($this->app['events']);
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
return $guard;
});
I change the guard in config/auth.php as
'guards' => [
'web' => [
'driver' => 'mysession',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
and finally my new guard
class MyGuard extends SessionGuard implements StatefulGuard, SupportsBasicAuth
{
/**
* #inheritdoc
*/
protected function cycleRememberToken(AuthenticatableContract $user)
{
$user->setRememberToken($token = Str::random(25));
$this->provider->updateRememberToken($user, $token);
}
}
Related
I'm hoping for a straight-forward solution to do this, but so far I've been coming up empty...
I have a front end vue app/form that sends data back to my laravel backend - I have a controller that validates and saves the request (not looking for feedback on this architecture at the moment unless it actually solves the problem - that's a task for another day...)
I've added a json column called "custom_redeem_fields"
For context, it's to support more flexibility and accepts key/val pairs to use in another field called "custom_redeem_instructions" that has text with delimiters for each of the keys from "custom_redeem_fields", although, I'd prefer to keep from defining these keys statically because the whole point is to be able to add new keys at will. So custom_redeem_instructions will read something like "please visit {•URL•} and enter code {•CODE•}..." and those values will come from the custom_redeem_fields json field.
In the model, I have "custom_redeem_fields" in the fillable array, as well as set as castable to json.
protected $fillable = ['custom_redeem_fields'];
protected $casts = ['custom_redeem_fields' => 'json'];
In the controller, I have ~20 additional columns (not really relevant here, so I've only included two) so I'm trying not to call them out individually beyond their validation rules. The request typically sends one field at a time, so the user can update and save each field as they go. This was working appropriately for all the other fields I had before I added the "custom_redeem_fields.xxxx" to the mix.
$validatedData = $request->validate([
'title' => 'sometimes|required|max:255',
'text' => 'sometimes|required_unless:redeem_type,9|max:255',
'custom_redeem_fields.email' => 'sometimes|email',
'custom_redeem_fields.phone' => ['sometimes', new ValidPhone],
'custom_redeem_fields.code' => 'sometimes',
'custom_redeem_fields.url' => 'sometimes|url'
]);
$ticket = Ticket::find($id)
$ticket->update($validatedData);
Now, with the "custom_redeem_fields.xxxxx" this falls apart - the entire json object stored in "custom_redeem_fields" is overwritten with the most recent update, rather than just updating the key included in the validatedData array. So if I save:
[
"title" => "Monty Pythons Flying Circus"
"text" => "Monty Pythons Flying Circus is a British surreal sketch comedy series created by and starring the comedy group Monty Python, consisting of Graham Chapman, ..."
"custom_redeem_fields" => [
"email" => "bob#example.com",
"phone" => "503.555.5555",
"code" => "1xoicvjq",
"url" => "https://example.com/"
]
]
and then I send:
"custom_redeem_fields" => ["email" => "pat#example.com"]
the custom redeem fields returns:
"custom_redeem_fields" => ["email" => "pat#example.com"]
rather than:
"custom_redeem_fields" => ["email" => "pat#example.com", "phone" => "503.555.5555", "code" => "1xoicvjq", "url" => "https://example.com/"]
It seems that validation rules need json keys to be notated with dot syntax (custom_redeem_fields.url), and eloquent needs arrow syntax (custom_redeem_fields->url), but I'm not sure what's the most straightforward way to transition between the two, which seems very not-laravel, and the documentation is certainly lacking in this department...
Any help would be appreciated.
Thanks!
Wouldn't array_merge() solve your problem, it would overwrite values you provide with the second parameter. If you give it the already existing ones as the first, it would combine the two as you want.
$customRedeemInput = [...];
$model->custom_redeem_fields = array_merge($model->custom_redeem_fields, $customRedeemInput);
$model->save();
I am working on Restful APIs of Yii.
My controller name is ProductsController and the Model is Product.
When I call API like this GET /products, I got the listing of all the products.
But, now I want to filter the records inside the listing API.
For Example, I only want those records Which are having a product name as chairs.
How to implement this?
How to apply proper filtering on my Rest API. I am new to this. So, I have no idea how to implement this. I also followed their documentation but unable to understand.
May someone please suggest me a good example or a way to achieve this?
First of all you need to have validation rules in your model as usual.
Then it's the controllers job and depending on the chosen implementation I can give you some hints:
If your ProductsController extends yii\rest\ActiveController
Basically the easiest way because almost everything is already prepared for you. You just need to provide the $modelClass there and tweak actions() method a bit.
public function actions()
{
$actions = parent::actions();
$actions['index']['dataFilter'] = [
'class' => \yii\data\ActiveDataFilter::class,
'searchModel' => $this->modelClass,
];
return $actions;
}
Here we are modifying the configuration for IndexAction which is by default responsible for GET /products request handling. The configuration is defined here and we want to just add dataFilter key configured to use ActiveDataFilter which processes filter query on the searched model which is our Product. The other actions are remaining the same.
Now you can use DataProvider filters like this (assuming that property storing the product's name is name):
GET /products?filter[name]=chairs will return list of all Products where name is chairs,
GET /products?filter[name][like]=chairs will return list of all Products where name contains word chairs.
If your ProductsController doesn't extend yii\rest\ActiveController but you are still using DataProvider to get collection
Hopefully your ProductsController extends yii\rest\Controller because it will already benefit from serializer and other utilities but it's not required.
The solution is the same as above but now you have to add it by yourself so make sure your controller's action contains something like this:
$requestParams = \Yii::$app->getRequest()->getBodyParams(); // [1]
if (empty($requestParams)) {
$requestParams = \Yii::$app->getRequest()->getQueryParams(); // [2]
}
$dataFilter = new \yii\data\ActiveDataFilter([
'searchModel' => Product::class // [3]
]);
if ($dataFilter->load($requestParams)) {
$filter = $dataFilter->build(); // [4]
if ($filter === false) { // [5]
return $dataFilter;
}
}
$query = Product::find();
if (!empty($filter)) {
$query->andWhere($filter); // [6]
}
return new \yii\data\ActiveDataProvider([
'query' => $query,
'pagination' => [
'params' => $requestParams,
],
'sort' => [
'params' => $requestParams,
],
]); // [7]
What is going on here (numbers matching the code comments):
We are gathering request parameters from the body,
If these are empty we take them from the URL,
We are preparing ActiveDataFilter as mentioned above with searched model being the Product,
ActiveDataFilter object is built using the gathered parameters,
If the build process returns false it means there is an error (usually unsuccessful validation) so we return the object to user to see list of errors,
If the filter is not empty we are applying it to the database query for Product,
Finally we are configuring ActiveDataProvider object to return the filtered (and paginated and sorted if applicable) collection.
Now you can use DataProvider filters just as mentioned above.
If your ProductsController doesn't use DataProvider to get collection
You need to create your custom solution.
I have a Laravel application which uses a lot of AJAX POST and GET requests (Single Page Application). Once an item is saved via POST, a GET request is sent to reload parts of the page and get any new data.
After enabling split read and write database connections using the Laravel connection configuration, the application runs incredibly quickly (never thought this would be a problem!). It saves and then requests so quickly that the RO database (reporting just 22ms behind) doesn't get chance to update and I end up with old information.
I have enabled the sticky parameter in the database configuration which I thought would mitigate the problem, but the POST and GET requests are separate so the stickiness gets lost.
I could rewrite a large portion of the application POST requests respond with the correct data, but this doesn't work for reloading many components at once and is an enormous job so I see this as a last resort.
Another idea I had was to modify the getReadPdo(){...} method and $recordsModified value inside the Database Connection class so that the stickiness is saved on the user's session for up-to 1 second. I was unsure if this would cause any further issues with speed or excessive session loading that it would cause more problems.
Has anyone experienced this before or have any ideas on how to tackle the problem?
Thanks in advance.
Thought I'd update and answer this in case anyone else came across the same issue.
This isn't a perfect solution but has worked well over the last week or so.
Inside the AppServiceProvider boot() method, I added the following
DB::listen(function ($query) {
if (strpos($query->sql, 'select') !== FALSE) {
if (time() < session('force_pdo_write_until')) {
DB::connection()->recordsHaveBeenModified(true);
}
} else {
session(['force_pdo_write_until' => time() + 1]);
}
});
In a nutshell, this listens to every DB query. If the current query is a SELECT (DB read), we check to see if the "force_pdo_write_until" key inside the user session has a timestamp that is more than the current time. If it is, we trick the current DB connection into using the ReadPDO by utilizing the recordsHaveBeenModified() method - this is how the core Laravel sticky sessions are normally detected
If the current query is not a SELECT (most likely a DB Write), we set the session variable for "force_pdo_write_until" for 1 second in the future.
Any time a POST request is sent, if the next GET request is within 1 second of the previous query, we can be sure that the current user will be using the RW DB connection and get the correct results.
Update (09/12/19):
It turns out the solution above doesn't actually modify the DB connection at all, it was just adding a few milliseconds of processing time to any request so looked like it was working about 75% of the time (because the DB replica lag fluctuates depending on load).
In the end I decided I'd go a bit deeper and override the DB connection class directly and modify the relevant functions. My Laravel instances uses MySQL, so I overrode the Illuminate\Database\MySqlConnection class. This new class was registered through a new service provider, which in turn is loaded through the config.
I've copied the config and files I used below to make it easier for any new developers to understand. If you're copying these directly, make sure you also add the 'sticky_by_session' flag to your connection config as well.
config/database.php
'connections' => [
'mysql' => [
'sticky' => true,
'sticky_by_session' => true,
...
],
],
config/app.php
'providers' => [
App\Providers\DatabaseServiceProvider::class
...
],
app/Providers/DatabaseServiceProvider.php
<?php
namespace App\Providers;
use App\Database\MySqlConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
if (config('database.connections.mysql.sticky_by_session')) {
Connection::resolverFor('mysql', function ($connection, $database, $prefix, $config) {
return new MySqlConnection($connection, $database, $prefix, $config);
});
}
}
}
app/Database/MySqlConnection.php
<?php
namespace App\Database;
use Illuminate\Database\MySqlConnection as BaseMysqlConnection;
class MySqlConnection extends BaseMysqlConnection
{
public function recordsHaveBeenModified($value = true)
{
session(['force_pdo_write_until' => time() + 1]);
parent::recordsHaveBeenModified($value);
}
public function select($query, $bindings = [], $useReadPdo = true)
{
if (time() < session('force_pdo_write_until')) {
return parent::select($query, $bindings, false);
}
return parent::select($query, $bindings, $useReadPdo);
}
}
Inside of recordsHaveBeenModified(), we just add a session variable for later use. This method is used by the normal Laravel sticky session detection, as mentioned previously.
Inside of select(), we check to see if the session variable was set less than a second ago. If so, we manually force the request to use the RW connection, otherwise just continue as normal.
Now that we're directly modifying the request, I haven't seen any RO race conditions or effects from the replica lag.
I've published as a package!
mpyw/laravel-cached-database-stickiness: Guarantee database stickiness over the same user's consecutive requests
Installing
composer require mpyw/laravel-cached-database-stickiness
The default implementation is provided by ConnectionServiceProvider, however, package discovery is not available.
Be careful that you MUST register it in config/app.php by yourself.
<?php
return [
/* ... */
'providers' => [
/* ... */
Mpyw\LaravelCachedDatabaseStickiness\ConnectionServiceProvider::class,
/* ... */
],
/* ... */
];
Thats all! All problems will be solved.
I'm using URL manager like the following:
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
'verses/view/<id:\d+>' => 'verses/view',
],
],
It works fine to make access using mysite.com/verses/view/158. The problem is, it is still possible to access the same content using non pretty URL i.e using plain get parameter such as mysite.com/verses/view?id=158. I need any way to restrict the access using the pretty URL.
I have tried the following couples of rules separately, but nothing I have gotten:
'verses/view<?id=>' => 'Error404',
'verses/view?id=<\d+>' => 'Error404',
What is the point of such restriction?
Anyway, one way to do it is something like this:
public function actionView($id)
{
if (strpos(\Yii::$app->request->getUrl(), '?') !== false) {
throw new \yii\web\BadRequestHttpException;
}
// ... the rest of action
}
No changes in UrlManager needed.
Try using UrlManager parameter enableStrictParsing = true.
What happens. UrlManager checks all rulls and they all do not match the request. Thus, by default it checks all default rules. Among default rules it finds the rule with ?id= and preforms routing to that one.
So, in order to avoid that route, you need to list all possible routes in the UrlManger rules and make enableStrictParsing = true. The routes not listed in the config rules parameter will be ignored.
I am working for a company who use tabulated html/JS interfaces. These are home grown (real honest to god s) with query events attached to each cell. For the old usage they were suitable, but the interactions required between rows and cells are becoming much more complex on the client side. Specifically they want both server and client side validation.
To facilitate this, the devs I report to are super keen on Zend_Forms, and insist that to use a framework like ExtJS, they don't want to have to write back end and front end code twice (please ignore that if it's all home grown they'll have to do this anyway).
So with that in mind, I'm trying to leverage Zend_Form decorators to create Ext.grid.Panel column defintions. For this, I would need to use decorators to export an array (and then json it using the ViewHelper), or render a JSON string directly.
So this would be something like:
$dateElement = new Zend_Form_Element_Text('startDate', array(
'label' => 'Start Date',
'validators' => array(
new Zend_Validate_Date()
)
));
echo (string)$dateElement;
would output:
{ text: 'Start Date', dataIndex:'startDate', xtype:'datecolumn'}
or (obviously not with string cast, but maybe with ->toArray() or something):
array( 'text' => 'Start Date', 'dataIndex' => 'startDate', 'xtype' => 'datecolumn')
I think if I could get it to this stage, I could get what I need out of it.
Has anyone here tried to do anything similiar to this (getting a JSON/XML/other markups output, rather than HTML from Zend_Forms using Decorators) or if they could point me to any resources?
I think I have a solution...
Make a decorator similar to this:
class My_Form_JSON_Decorator extends Zend_Form_Decorator_Abstract{
protected $xtype;
protected $dataIndex;
public function __construct($dataIndex,$xtype){
$this->xtype=$xtype;
$this->dataIndex=$dataIndex;
}
public function render($content){
$element=$this->getElement();
$label=$element->getLabel
//if you need errors here too do the same with $element->getMessages();
return 'array ("text"=>"'.$label.'","dataIndex"=>"'.$this->dataIndex.'","datecolumn"=>"'.$this->xtype.'")';
}
}
Then, on the form, use something similar to this:
$dateElement = new Zend_Form_Element_Text('startDate', array(
'label' => 'Start Date',
'validators' => array(
new Zend_Validate_Date()
)
$dateElement->setDecorators(array(
new My_Form_JSON_Decorator("startDate","datecolumn");
));
And finally, on the View, you should have this:
{
Date: <?php echo $this->form->startDate; ?>,
}
I didn't tried the code above but, I did it with a similar code I used once when I needed to change Decorators of a Form.
It could not be all correct but, I think that it shows you a way of doing that.
Good work =)