Doctrine does not update FOSUserBundle User object - symfony

I'm encountered a weird problem.
I'm working on a Symfony 2.1 project with Doctrine 2.2 and the FOSUserBundle for user management.
I added a RequestListener, since the user can change the language of the site and I want to track the last used language of the user.
So I simply added a new property to the User Entity and then want to save the new language if has changed.
So I'm doing this in the request listener:
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) {
return;
}
if ($event->getRequest()->getRequestFormat() !== 'html') {
return;
}
if ($this->context->getToken()->getUser() instanceof \Foo\BarBundle\Entity\User) {
$this->request = $event->getRequest();
$this->user = $this->context->getToken()->getUser();
if ($this->user->getCustomer() instanceof \Foo\BarBundle\Entity\Customer) {
$this->customer = $this->user->getCustomer();
$permission = $this->permissionService->getPermissionSafely($this->customer);
$params = $this->request->get('_route_params');
$language = $this->getLanguage($permission['language']['languages']);
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'en_US';
if ($language !== "all" && $this->request->get('_locale') !== $language) {
$params['_locale'] = $language;
$redirect = new RedirectResponse($this->router->generate($this->request->get('_route'), $params));
$event->setResponse($redirect);
}
if ($this->user->getLastLanguage() !== $locale) {
$this->user->setLastLanguage($locale);
$this->em->flush();
}
}
}
}
private function getLanguage($language)
{
if (!isset($language['en'])) {
return 'de';
}
if (!isset($language['de'])) {
return 'en';
}
if ($language['en'] && !($language['de'])) {
return 'en';
} else if (!$language['en'] && $language['de']) {
return 'de';
}
return 'all';
}
Important is the last if-conditional. If the current $locale is different than the last used, I want to update the user object. So there are three possible values: de_DE, en_US and null.
Now the weird behaviour comes in (and I don't know if it's a bug or what, but I'm confused):
It doesn't matter which value is stored in the database, it always gets updated to en_US.
If a user has visit the page for the first time (value null) and visits the site in german (value de_DE) it gets updated to en_US, but the profiler query says:
UPDATE `user` SET last_language = 'de_DE' WHERE id = 1
If a user has last_language = 'de_DE' and visits the site in german (de_DE) it gets updated to en_US, but the query profiler says, that there wasn't a update query. Which makes sense, because the $locale is the same like $this->user->getLastLanguage().
What the??
I have no idea what is going on here. Has anyone experienced a similar problem? Has this something to do with the fact, that I'm modifying the user object from the security context?
Update: The funny thing is, if I change line
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'en_US';
to
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'es_US';
it gets updated to es_US event if $locale holds de_DE

Your ternary if statement will always fail because the return value of
$this->request->get('_locale')
will be :
de_DE
but never === 'de'. Therefore if you save $locale in your entity after calling
$locale = (strtolower($this->request->get('_locale')) === 'de') ? 'de_DE' : 'en_US';
... the next time this statement fails and puts it back to 'en_US' calling ...
$user->setLastLanguage('en_US');
... in the end. Just do a better comparison like ...
$locale = (strstr($locale,'de') !== false) ? 'de_DE' : 'en_US';
Have you tried persist before flushing:
$this->user->setLastLanguage($locale);
$this->em->flush();
should be ...
$this->user->setLastLanguage($locale);
$this->em->persist($this->user);
$this->em->flush();
... if your user is newly created and not already managed by doctrine.

Related

How to Save Multiple User Metadata in Database?

I am trying to create a registration form (here) with the help of a free plugin.
There are many elements in the registration form.
In the admin panel, meta keys can be assigned for each element in the plugin interface. So there is such an opportunity.
I'm trying to collect many of the elements on the same meta key by taking advantage of this possibility. In this direction, I gave the common meta key value to the elements I created. For example: info_about_register
So far everything is fine. However, if the form is posted, I can only get the last entry in the usermeta table. So the plugin is not serializing the same meta key data. An array does not occur.
There are many form elements. I want to pull these to the admin panel later. Therefore, I think that defining a separate line for each element will tire the system a lot. I contacted the plugin developers about this issue. However, no response for about 1.5 weeks.
I tried to solve this problem myself and found the codes where the action was taken. I made some changes to these. However, I was not successful. I would be very happy if you guide me.
.../includes/class-frontend.php
/*
* Called after submission save
* Registers new user into WordPress.
* Also map field values to user meta (If configured)
*/
public function after_submission_insertion($errors, $submission, $data) {
$sub_model = erforms()->submission;
$form_model = erforms()->form;
$form = $form_model->get_form($submission['form_id']);
// Copy attachment values in data from submission (as $data does not have any uploaded file values)
if(!empty($submission['attachments'])){
foreach($submission['attachments'] as $attachment){
if(!isset($data[$attachment['f_name']])){
$data[$attachment['f_name']]= $attachment['f_val'];
}
}
}
if ($form['type'] == "reg") { // Handling of registration forms
$user = 0;
$id = 0;
// Get mapping for user meta fields if any
$user_field_map = erforms_filter_user_fields($form['id'], $submission['fields_data']);
// Avoid user registration process if user already logged in
if (!is_user_logged_in()) {
$email_or_username = $user_field_map['user_email'];
if (isset($user_field_map['password'])) {
// Silently creates user
$username = isset($user_field_map['username']) ? $data[$user_field_map['username']] : $data[$email_or_username];
do_action('erf_before_user_creation',$submission);
$id = wp_create_user($username, $data[$user_field_map['password']], $data[$email_or_username]);
} else {
// Register user and sends random password via email notification
do_action('erf_before_user_creation',$submission);
$id = register_new_user($data[$email_or_username], $data[$email_or_username]);
}
if (is_wp_error($id)) {
// In case something goes wrong delete the submission
wp_delete_post($submission['id'], true);
$error_code = $id->get_error_code();
if ($error_code == 'existing_user_login') {
$email_or_username = 'username_error';
}
$errors[] = array($email_or_username, $id->get_error_message($id->get_error_code()));
return $errors;
} else {
$selected_role = erforms_get_selected_role($submission['form_id'], $data);
if (!empty($selected_role)) { // Means user has selected any role
$user_model = erforms()->user;
$selected_role= apply_filters('erf_before_setting_user_role',$selected_role,$id,$form, $submission);
$user_model->set_user_role($id, $selected_role);
}
foreach ($user_field_map as $req_key => $meta_key) {
$is_primary_key = in_array($meta_key, erforms_primary_field_types());
if (isset($data[$req_key]) && !$is_primary_key) {
$m_keys= explode(',',$meta_key);
foreach($m_keys as $m_key){
if(!empty($m_key)){
$status = erforms_update_user_meta($id, $m_key, $data[$req_key]);
do_action('erf_user_meta_updated',$m_key,$id,$data[$req_key],$status);
}
}
}
}
do_action('erf_user_created', $id, $form['id'], $submission['id']);
}
} else {
// Get user details
$user = wp_get_current_user();
$id = $user->ID;
foreach ($user_field_map as $req_key => $meta_key) {
$is_primary_key = in_array($meta_key, erforms_primary_field_types());
if (isset($data[$req_key]) && !$is_primary_key) {
$m_keys= explode(',',$meta_key);
foreach($m_keys as $m_key){
if(!empty($m_key)){
$status= erforms_update_user_meta($id,$m_key,$data[$req_key]);
do_action('erf_user_meta_updated',$m_key,$id,$data[$req_key],$status);
}
}
}
}
//...
// User meta,URL params or default values should be prefilled only when we are not loading submission data
if(empty($submission)){
$user_meta = erforms()->user->frontend_localize_user_meta($form);
$filtered_url_params = array();
foreach ($_GET as $key => $val) {
$filtered_url_params[urldecode(strtolower(wp_unslash($key)))] = sanitize_text_field(wp_unslash($val));
}
$url_keys = array_keys($filtered_url_params);
foreach ($form['fields'] as $field) {
$label = !empty($field['label']) ? strtolower(str_replace(' ', '_', $field['label'])) : '';
$label = str_replace('&', 'and', $label); // Cause URL params do not allow &
if (!empty($field['name']) && !empty($label) && in_array($label, $url_keys)) {
if (!isset($user_meta[$field['name']]) && !empty($filtered_url_params[$label])) {
$user_meta[$field['name']] = stristr($filtered_url_params[$label], '|') ? explode('|', $filtered_url_params[$label]) : $filtered_url_params[$label];
}
}
if(!empty($field['name']) && empty($user_meta[$field['name']]) && !empty($field['value'])){
$user_meta[$field['name']] = $field['value'];
}
}
if (!empty($user_meta)) {
$data['user_meta'] = $user_meta;
}
}
$data= apply_filters('erf_form_localize_data',$data,$form);
return $data;
}
.../includes/functions.php
/**
* Wrapper to call update_user_meta function.
* This simply calls wordpress meta function and does not add any special prefix.
* Checks for any special meta key to update user table data.
* For example: display_name : updates user's display name. Instead of adding display_name usermeta
*/
function erforms_update_user_meta($user_id, $m_key, $m_val) {
switch ($m_key) {
case 'display_name' : $status = wp_update_user(array('ID' => $user_id, $m_key => $m_val));
return is_wp_error($status) ? false : true;
}
return update_user_meta($user_id, $m_key, $m_val);
}
Update:
Data can be stored as an array with a definition like below. Also, the key values ($all_meta_value[$req_key]) are equal to the id, name values automatically assigned to the HTML elements by the plugin.
Using this, different conditional states can be written.
The following code must be defined in the function.php file in the child theme:
add_action('erf_user_meta_updated','for_new_user_meta_uptated',10);
function for_new_user_meta_uptated($data){
//if the defined meta key (info_about_register) matches
if($m_key == 'info_about_register'){
$all_meta_value[$req_key] = $data;
}
}
add_action('erf_user_created', 'for_new_user_created',10);
function for_new_user_created($id){
//Save the values as a new user meta
add_user_meta($id, 'new_info_about_register', $get_meta_value);
//Remove saved single element user meta.
delete_user_meta($id, 'info_about_register');
}

swiftmail symfony duplicate check for error log / email before sending

I would like to run a duplicate content check just before firing off an email whcih uses swiftmailer inside my symph2 app to send me dev ERROR log entries.
this functionality sits right next to my error log to database function, where it too has a duplicate check, although that one is much easier, it uses sql.
for this one, i want to maintain the last mail sent body for atleast the next 10 emails sent, so that if my error log goes out of control, it wont keep firing me duplicate emails of the same error.
should i just collect this body onto an object that holds last 10 email bodies, and attach this to the swift mailer class? or is there an easier way, like using something that is already embedded in swift mailer for this kind of post sending use? Or maybe a session..
Edit, i call swift mailer from a backend helper class, so think i can pretty much do anything there so long as its atleast semi-elegant.
EDIT this is a refined version of the method that calls both the persist and firing of email
<?php
class someWierdClass
{
public function addLogAction(Request $request, $persist = TRUE, $addEmail = TRUE)
{
$responseAdd = array();
if ($this->getRequest()->request->all() !== null) {
$data = $this->getRequest()->request->get('data') ? $this->getRequest()->request->get('data') : 'no_data';
$duplicate = $this->getRequest()->request->get('duplicate', null);
}
if ($addEmail) {
$responseAdd[] = 'firedIt';
$this->fireEmailString('You have an error log here. <br>' . $data);
}
if ($persist)
{
$responseAdd[] = 'persistedIt';
$this->persistLog($data, $duplicate);
}
if ($responseAdd)
{
$body = implode(', ', $responseAdd);
return new Response($body);
}
}
}
Log emails in a table and check that there it isn't a duplicate every time you send an email.
To do this, you should create a helper function that queries the emails table for entries who's body matches the body you would like to send. If the query returns nothing, then you know that isn't a duplicate. You would then send the email and log it the database. Otherwise, if it returned (a) record(s), you would send a dev ERROR log entry.
If you would like to only check against the last 10 emails, you would do this by querying for both $body == $new_body and $id >= ($total_rows-10)
You would then inject this into the container and call it using something like this
$this->container->get('helper')->sendEmail($body, $subject, $recipients);
Ok, thanks Dan for the idea as to using the database to do the dup check. If you notice, per your suggestion, i was already doing the dup check, but it made me think. It helped me connect the dots.
What i have done is return the answer if its a duplicate on the response when it does the updating the database, then using that response as a flag to determine if email fires or not. (in my case, i go further to check the updated stamp is at least +1 hour old, as opposed to the 'last 10 emails content' idea)
Heres the code.. Enjoy..
<?php
namespace Acme\AcmeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller,
Acme\AcmeBundle\Entity\Log,
Symfony\Component\HttpFoundation\JsonResponse,
Symfony\Component\HttpFoundation\Response,
Symfony\Component\Config\Definition\Exception\Exception,
Symfony\Component\HttpFoundation\Request,
Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
class someWierdClass
{
/**
* #var array
*/
protected $senderArray = array('no-reply#yorudomain.com' => 'Your Website Name');
/**
* #param Request $request
* #param bool $persist
* #param bool $addEmail
* #return Response
*/
public function addLogAction(Request $request, $persist = TRUE, $addEmail = TRUE)
{
$responseAdd = array();
if ($this->getRequest()->request->all() !== null) {
$data = $this->getRequest()->request->get('data') ? $this->getRequest()->request->get('data') : 'no_data';
$type = $this->getRequest()->request->get('type') ? $this->getRequest()->request->get('type') : 'no_type';
$duplicate = $this->getRequest()->request->get('duplicate', null);
}
if ($addEmail) {
$responseAdd[] = 'firedIt';
$this->fireEmailString('You have an error log here. <br>' . $data);
}
if ($persist) {
$responseAdd[] = 'persistedIt';
$persistResponse = $this->persistLog( $type = $data, $duplicate);
if ($persistResponse) {
// a dup check is done here and results of this is on the response. (e.g. $content->passesCutoff)
$content = json_decode($persistResponse->getContent());
}
}
if ( $addEmail && ( isset($content->passesCutoff) && $content->passesCutoff ))
{
//fire off an email also, because its kind of hard to look in teh logs all the time, sometimes we just want an email.
$successEmail = $this->fireEmailString($data);
if( ! $successEmail )
{
$responseAdd[] = 'firedIt';
}
}
if ($responseAdd) {
$body = implode(', ', $responseAdd);
return new Response($body);
}
}
/**
* #param $emailStringData
* #param null $emailSubject
* #param null $emailTo
* #return mixed
*/
protected function fireEmailString($emailStringData, $emailSubject = null, $emailTo=null){
$templateName = 'AcmeBundle:Default:fireEmailString.html.twig';
if( ! $emailSubject )
{
$emailSubject = 'An email is being fired to you!' ;
}
if( ! $emailTo )
{
$emailTo = 'youremail#gmail.com';
}
$renderedView = $this->renderView(
$templateName, array(
'body' => $emailStringData,
));
$mailer = $this->get('mailer');
$message = $mailer->createMessage()
->setSubject( $emailSubject)
->setBody($emailStringData, 'text/plain')
->addPart($renderedView, 'text/html')
->setFrom($this->senderArray)
->setSender($this->senderArray)
->setTo($emailTo);
$results = $mailer->send($message);
return $results;
}
/**
* #param $type
* #param $data
* #param $duplicate
* #return JsonResponse
*/
protected function persistLog($type, $data, $duplicate) {
$em = $this->getDoctrine()->getManager();
$count = null;
$passesCutoff = null;
$mysqlNow = new \DateTime(date('Y-m-d G:i:s'));
//only two conditions can satisy here, strings '1' and 'true'.
if($duplicate !== '1' && $duplicate !== 'true' /*&& $duplicate != TRUE*/)
{
//in order to check if its unique we need to get the repo
//returns an object (findByData() would return an array)
$existingLog = $em->getRepository('AcmeBundle:Log')->findOneByData(
array('type' => $type, 'data' => $data)
);
if($existingLog)
{
$timeUpdatedString = strtotime($existingLog->getTimeupdated()->format('Y-m-d H:i:s'));
$cutoffStamp = strtotime('+1 hour', $timeUpdatedString); //advance 1 hour (customize this to the amount of time you want to go by before you consider this a duplicate. i think 1 hour is good)
$passesCutoff = time() >= $cutoffStamp ? TRUE : FALSE; //1 hour later
$count = $existingLog->getUpdatedcount();
$existingLog->setUpdatedcount($count + 1); // '2014-10-11 03:52:20' // date('Y-m-d G:i:s')
$em->persist($existingLog);
}
else
{
//this record isnt found, must be unique
$newLog = new Log(); //load our entity
//set in new values
$newLog->setType($type);
$newLog->setData($data);
$newLog->setUpdatedcount(0);
$newLog->setTimeupdated($mysqlNow);
$em->persist($newLog);
}
}
else
{
//we dont care if unique or not, we just want a new row
$newLog = new Log(); //load our entity
$newLog->setType($type);
$newLog->setData($data);
//time updated has been set to auto update to current timestamp in the schema, test first, then remove this
$newLog->setUpdatedcount(0);
$newLog->setTimeupdated($mysqlNow);
$em->persist($newLog);
}
$em->flush();
$response = new JsonResponse();
$response->setData(
array(
'data' => 'persistedIt',
'existingLog' => $count,
'passesCutoff' => $passesCutoff,
));
return $response;
}
}
In hindsight, i would have just passed the last update timestamp back on the response from the persist method, then do the cutoff calculation inside the fire email method obviously, but the above code does work as a starting point.. :-)

Return entity record value based on custom value in Symfony2

I'm making an API and I need to display data from entity based on action type. For example, I have User and his visibility preferences (to hide/show his name for other people). Doing this like that:
<?php
// entity
public function getSurname()
{
$visibility = $this->getVisibility();
if($visibility['name'] == 0)
return $this->surname;
return '';
}
is ok, but if User is logged in, I want to show him his name, for example, in edit account.
The best way I think is to edit record when I get it from database, but how to this on doctrine object?
<?php
//controller
$user = $this->getDoctrine()->getRepository('AcmeDemoBundle:User')->findOneById($id);
$user = $this->getVisibility();
if($user != $this->getUser() && $visibility['name'] == 0)
$user->setSurname(''); //but this save this to DB, not to "view"
UPDATE
Unfortunately (or I'm doing something wrong) my problem can't be solved by Snake answer, beause when I do this code:
<?php
$user = $this->getDoctrine()->getRepository('AcmeDemoBundle')-findOneById($id);
return array(
self::USER => $user
);
In my API response, entity modifications don't work, because I think this is getting record directly from DB? And I need return whole object like in code above.
UPDATE2
I found workaround for this
<?php
// entity
/**
* #ORM\PostLoad
*/
public function postLoad() {
$this->surname = $this->getSurname();
}
and then I can just return full $user object
If you want to show the surname depends of visibility, you can add the Symfony\Component\Security\Core\User\EquatableInterface and edit your function:
// entity
public function getSurname(Acme\DemoBundle\User $user = null)
{
// Nothing to compare or is the owner
if( !is_null( $user ) && $this->isEqualTo($user) ){
return $this->surname;
}
// else...
$visibility = $this->getVisibility();
if($visibility['name'] == 0)
return $this->surname;
return '';
}
After in your controller you only must get the surname:
//controller
$user = $this->getDoctrine()->getRepository('AcmeDemoBundle:User')->findOneById($id);
// If the user is the owner, show the surname, otherwise it shows the surname depends of visibility
$surname = $user->getSurname( $this->getUser() );
Also, you can execute the logic in the controller (check if is the same user and get the visibility...).
I suggest you read about ACL too.

Drupal 7 with Multiple Login sources?

I'm just a new to Drupal but i wanna make my site to be authenticated from external login sources. I mean, not to use the current Drupal database to login. Then use one or more external databases.
To make it a little more clear:
User Login Authentication will not use the existing Drupal database.
Then it will use the external database(s)
External Database will be one or more (If User is not found on one external database, then found on another source again.)
I've been trying to hack the Drupal Login but i can't make it yet as the structure is complicated enough. Any solution or suggestion please?
Which files will be needed to modify?
You shouldn't completely discard Drupal's user system for this, but rather piggy tail on it with your own authentication. The way this works is by 1) collecting user credentials, 2) authenticating against your custom sources and 3) registering/logging in an external user.
Collecting credentials
One way of collecting user credentials is by hijacking the user login form. You do this by implementing hook_form_alter() and add your own validation callback.
function mymodule_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'user_login') {
$form['#validate'][] = array('mymodule_user_login_validate');
$form['#validate'][] = array('mymodule_user_login_submit');
}
}
The login form will look exactly the same and but it will send the username and password to your callback instead.
The user_login_name_validate() and user_login_final_validate() callbacks are user.module's default validators. They supply user blocking functionality and brute force protection but feel free to leave them out.
Authentication
When the login form is sent you pick up the credentials and authenticate against your custom sources.
function mymodule_user_login_validate($form, &$form_state) {
mymodule_authenticate(
$form_state['values']['name'],
$form_state['values']['name']
);
}
If the credentials doesn't check out you can just call form_set_error() to inform the user of what went wrong. Doing this will also make sure no further validation or submit callbacks are executed.
External users
So if no error is evoked then Drupal will move on to running your submit callback. Then we can run user_external_login_register() to create our external user as a Drupal user.
function mymodule_user_login_submit($form, &$form_state) {
user_external_login_register($form_state['values']['name'], 'mymodule');
}
As the name implies, this function will also log the user in. And that should be it!
There's a module for that
Chances are there is already a module for what you want to do. Always go search for existing contrib modules before starting your own!
define('DRUPAL_ROOT', 'full/path/to/drupal');
$drupal_url = 'http://site.com';
drupal_external_load($drupal_path, $drupal_url);
if ( !($user->uid > 0) ) {
drupal_external_login('user', 'pass');
} else {
print 'user is already logged';
}
function drupal_external_load($drupal_url) {
global $base_url;
// set drupal base_url (if not set set in settings.php)
// because it's used in session name
$base_url = $drupal_url;
// save current path
$current_path = getcwd();
// move to drupal path, because it uses relative path for its includes
chdir(DRUPAL_ROOT);
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
// to use the drupal global user var (instead of session hack)
// drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
// return the current path
chdir($current_path);
}
// load a drupal user into the user obj
function drupal_external_userload($user_info = array()) {
// Dynamically compose a SQL query:
$query = array();
$params = array();
if (is_numeric($user_info)) {
$user_info = array('uid' => $user_info);
}
elseif (!is_array($user_info)) {
return FALSE;
}
foreach ($user_info as $key => $value) {
if ($key == 'uid' || $key == 'status') {
$query[] = "$key = %d";
$params[] = $value;
}
else if ($key == 'pass') {
$query[] = "pass = '%s'";
$params[] = md5($value);
}
else {
$query[]= "LOWER($key) = LOWER('%s')";
$params[] = $value;
}
}
$result = db_query('SELECT * FROM {users} u WHERE '. implode(' AND ', $query), $params);
if ($user = db_fetch_object($result)) {
$user = drupal_unpack($user);
$user->roles = array();
if ($user->uid) {
$user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
}
else {
$user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
}
$result = db_query('SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON
ur.rid = r.rid WHERE ur.uid = %d', $user->uid);
while ($role = db_fetch_object($result)) {
$user->roles[$role->rid] = $role->name;
}
//user_module_invoke('load', $user_info, $user);
}
else {
$user = FALSE;
}
return $user;
}
// don't send any headers before calling this
function drupal_external_login($username, $password) {
global $user;
if ( $user->uid > 0 ) {
if ( $user->name != $username ) {
drupal_external_logout();
} else {
return true;
}
}
require DRUPAL_ROOT. '/includes/password.inc' ;
$account = user_load_by_name($username);
if ( user_check_password($password, $account) ) {
$user = $account;
drupal_session_regenerate();
return true;
}
return false;
}
function drupal_external_logout() {
global $user;
session_destroy();
$user = drupal_anonymous_user();
}

How to use class-scope aces in Symfony2?

I've got a problem with class-scope aces. I've created an ace for a
class like this :
$userIdentity = UserSecurityIdentity::fromAccount($user);
$classIdentity = new ObjectIdentity('some_identifier', 'Class\FQCN');
$acl = $aclProvider->createAcl($classIdentity);
$acl->insertClassAce($userIdentity, MaskBuilder::MASK_CREATE);
$aclProvider->updateAcl($acl);
Now, I'm trying to check the user's permissions. I've found this way
of doing things, which is not documented, but gives the expected
results on a class basis :
$securityContext->isGranted('CREATE', $classIdentity); // returns true
$securityContext->isGranted('VIEW', $classIdentity); // returns true
$securityContext->isGranted('DELETE', $classIdentity); // returns false
This method is well adapated to the "CREATE" permission check, where
there's no available object instance to pass to the method. However,
it should be possible to check if another permission is granted on a
particular instance basis :
$entity = new Class\FQCN();
$em->persist($entity);
$em->flush();
$securityContext->isGranted('VIEW', $entity); // returns false
This is where the test fails. I expected that an user who has a given
permission mask on a class would have the same permissions on every
instance of that class, as stated in the documentation ("The
PermissionGrantingStrategy first checks all your object-scope ACEs if
none is applicable, the class-scope ACEs will be checked"), but it
seems not to be the case here.
you are doing it right. and according to the bottom of this page, it should work, but it does not.
the easiest way to make it work is creating an AclVoter class:
namespace Core\Security\Acl\Voter;
use JMS\SecurityExtraBundle\Security\Acl\Voter\AclVoter as BaseAclVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Doctrine\Common\Util\ClassUtils;
class AclVoter extends BaseAclVoter
{
public function vote( TokenInterface $token , $object , array $attributes )
{
//vote for object first
$objectVote = parent::vote( $token , $object , $attributes );
if( self::ACCESS_GRANTED === $objectVote )
{
return self::ACCESS_GRANTED;
}
else
{
//then for object's class
$oid = new ObjectIdentity( 'class' , ClassUtils::getRealClass( get_class( $object ) ) );
$classVote = parent::vote( $token , $oid , $attributes );
if( self::ACCESS_ABSTAIN === $objectVote )
{
if( self::ACCESS_ABSTAIN === $classVote )
{
return self::ACCESS_ABSTAIN;
}
else
{
return $classVote;
}
}
else if( self::ACCESS_DENIED === $objectVote )
{
if( self::ACCESS_ABSTAIN === $classVote )
{
return self::ACCESS_DENIED;
}
else
{
return $classVote;
}
}
}
return self::ACCESS_ABSTAIN;
}
}
then in security.yml set this:
jms_security_extra:
voters:
disable_acl: true
and finally set up the voter as a service:
core.security.acl.voter.basic_permissions:
class: Core\Security\Acl\Voter\AclVoter
public: false
arguments:
- '#security.acl.provider'
- '#security.acl.object_identity_retrieval_strategy'
- '#security.acl.security_identity_retrieval_strategy'
- '#security.acl.permission.map'
- '#?logger'
tags:
- { name: security.voter , priority: 255 }
- { name: monolog.logger , channel: security }
You need to ensure each object has its own ACL (use $aclProvider->createAcl($entity)) for class-scope permissions to work correctly.
See this discussion: https://groups.google.com/forum/?fromgroups=#!topic/symfony2/pGIs0UuYKX4
If you don't have an existing entity, you can check against the objectIdentity you created.
Be careful to use "double-backslashes", because of the escaping of the backslash.
$post = $postRepository->findOneBy(array('id' => 1));
$securityContext = $this->get('security.context');
$objectIdentity = new ObjectIdentity('class', 'Liip\\TestBundle\\Entity\\Post');
// check for edit access
if (true === $securityContext->isGranted('EDIT', $objectIdentity)) {
echo "Edit Access granted to: <br/><br/> ";
print_r("<pre>");
print_r($post);
print_r("</pre>");
} else {
throw new AccessDeniedException();
}
That should work!
If you would check for "object-scope" you could just use $post instead of $objectIdentity in the isGranted function call.
I have tried to find the best solution for this problem and I think the best answer is the one of rryter / edited by Bart. I just want to extend the solution.
Let's say you want to give access to a specific object type for a specific user, but not for a concrete object instance (for example id=1).
Then you can do the following:
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = new ObjectIdentity('class', 'someNamspace\\SeperatedByDoubleSlashes');
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$securityContext = $this->get('security.context');
$user = $securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
$securityContext = $this->get('security.context');
// check for edit access
if (false === $securityContext->isGranted('EDIT', $objectIdentity)) {
throw new AccessDeniedException();
}
The difference to the example given by the symfony cookbook is that you are using the class scope and not the object scope.
There's only 1 line that makes the difference:
$objectIdentity = new ObjectIdentity('class', 'someNamspace\\SeperatedByDoubleSlashes');
instead of:
$objectIdentity = ObjectIdentity::fromDomainObject($object);
You can still add specific permissions for specific object instances if you have at least one class scope permission in your classes acl.

Resources