When we log in a user via facebook we can save the facebook user id to the user profile.
It would come in handy if either the drupal or the facebook email adress changes.

It would also be nice if we can transfer the functionality to create a new account into a separate function.

Comments

func0der’s picture

Status: Active » Needs review

I see, that the patch includes the saving of the Facebook user id and the separation of the registration with a Facebook account.

This is a pretty good idea, because it is easier to register users coming from an API.

Thanks for the work.

masipila’s picture

It would also be nice if we can transfer the functionality to create a new account into a separate function.

The 2.x branch of this module has been refactored so that the code is divided into manageable pieces.

See also #2386041: Documentation is needed about difference between 1.x or 2.x branch for further discussion on different branches.

Cheers,
Markus

func0der’s picture

Same here: Since 2.x is not going to be stable anytime soon, this should be merged in 1.x.

  • masipila committed a5f3d0b on 7.x-2.x
    Issue #2387073 by bellhof, masipila: Additionally save facebook user id
    
masipila’s picture

Version: 7.x-1.14 » 7.x-2.x-dev
Status: Needs review » Fixed

Fix committed to 7.x-2.x-dev. Many thanks bellhof for the patch, I made minor updates to port your patch to 7.x-2.x branch.

7.x-2.x-beta1 will be released soon. If you want to test 7.x-2.x-dev, note that it uses Facebook PHP SDK v4 instead of v3 like 7.x-1.x. Also remember to run update.php so that the new database column will be created.

Cheers,
Markus

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

masipila’s picture

Status: Closed (fixed) » Closed (won't fix)

I reverted the commit which introduced the capability to store FBID after discussing this feature with the other module maintainer saitanay. We concluded that we want to keep this module as simple as possible and the matching by email address is sufficient for this simple module.

This design decision means that if the user changes her email address to her Facebook profile and then logs back to the Drupal site, a new Drupal user account will be created. Avoiding this corner case would mean storing the FBID to our Drupal database and altering the database schema. That is technically doable, but we concluded with saitanay that it is better to refrain from modifying the database schema.

Markus

func0der’s picture

Then I declare you module from now on as broken.
That philosophy is plain wrong.
Attached user data to an account like posts or favourites or subscriptions or whatever would be gone as soon as the user decides to change the Facebook email address.

If you do not want to alter the database schema, which is totally fine by the way, because it is not non-inversive, then create a new table that is responsible for the matching.

Do you really favour simplicity over correct and consistent behaviour here?

saitanay’s picture

hi func0der,

There are amazing modules like https://www.drupal.org/project/fbconnect and https://www.drupal.org/project/hybridauth available if you are considering any sophisticated integrations.

This module has always tried to remain as simple as possible, with a plug-and-play approach that you can switch on or switch off without affecting any other social integration or customizations you already have wrt user login or registration. The module also tries not to make any changes to the database.

I agree that the rare scenario of changing email address on facebook is not handled by the module.

The expected behaviour when you change your facebook email address by this module:
1) The user will fail to login with a clear message that no matching email address was found (I believe we display such a message already. We can if it's not already)
2) The user should login via drupal (either using the password, or by using the "Forgot Password" option and update his email address accordingly on the Drupal Site.

People rarely change their facebook email address and to favor the simplicity of this module, we had to take an informed decision to not handle this scenario.

Thank you
Tanay

kopeboy’s picture

Title: Additionally save facebook user id » Save facebook user id
Status: Closed (won't fix) » Needs review

I think the Facebook user ID is much more useful than in that corner-case scenario.

What if I want to provide direct links to message a Drupal user on Facebook..

I mean, after connecting with FB, having the user identification from Facebook is something I would expect! No more, but at least that.

Also, keep in mind that fbconnect module has only 1.5k reported installs after 250k downloads! Clearly there is something wrong there..

masipila’s picture

Regarding messaging link (and tons of other use cases): you would definitely need a separate module for that functionality. That module can easily implement the hook provided by Simple FB Connect and store the ID. This is exactly why we provide an API for other modules so that it is easy to extend the integrations to FB exactly how you want. See https://www.drupal.org/node/2475401 and https://www.drupal.org/node/2475445

Regarding fbconnect module: that module is deprecated in many ways, I do not recommend using it.

Cheers,
Markus

masipila’s picture

Status: Needs review » Closed (won't fix)

Changing the status back to wontfix

toemaz’s picture

For those are in need for a D8 auth solution using fb id rather than the email, add your voice in social_auth_facebook https://www.drupal.org/node/2788605#comment-11540915

swentel’s picture

Version: 7.x-2.x-dev » 8.x-3.x-dev

For anyone hacking on the D8 version, here's a way to use the facebook id as canonical to login, you don't need to hack in the module at all. I still think this should be the way to go as well, and it's also dead easy todo.

Add a facebook_id base field.

/**
 * Implements hook_entity_base_field_info().
 */
function yourmodule_entity_base_field_info(EntityTypeInterface $entity_type) {
  $fields = array();

  if ($entity_type->id() === 'user') {
    $fields['facebook_id'] = BaseFieldDefinition::create('string')
      ->setLabel('Facebook ID')
      ->setName('facebook_id')
      ->setDefaultValue('');
  }

  return $fields;
}

In a RouteSubscriber, take over the controller.

    // Change controller of simple connect facebook.
    if ($route = $collection->get('simple_fb_connect.return_from_fb')) {
      $route->setDefault('_controller', '\Drupal\yourmodule\Controller\FacebookController::returnFromFb');
    }

The Controller looks like this, the part that is different is were we pass on the facebook_id in the createUser method, or load by facebook_id instead of email.


namespace Drupal\yourmodule\Controller;

use Drupal\simple_fb_connect\Controller\SimpleFbConnectController;
use Symfony\Component\HttpFoundation\RedirectResponse;

class FacebookController extends SimpleFbConnectController {

  /**
   * Response for path 'user/simple-fb-connect/return'.
   *
   * Facebook returns the user here after user has authenticated in FB.
   */
  public function returnFromFb() {
    // Try to get an instance of Facebook service.
    if (!$facebook = $this->fbFactory->getFbService()) {
      drupal_set_message(t('Simple FB Connect not configured properly. Contact site administrator.'), 'error');
      return $this->redirect('user.login');
    }

    // Facebook service was returned, inject it to $fbManager.
    $this->fbManager->setFacebookService($facebook);

    // Read user's access token and save it to session for other modules.
    if (!$this->fbManager->saveAccessToken()) {
      drupal_set_message(t("Facebook login failed."), 'error');
      return $this->redirect('user.login');
    }

    // Get user's FB profile from Facebook API.
    if (!$fb_profile = $this->fbManager->getFbProfile()) {
      drupal_set_message(t("Facebook login failed, could not load Facebook profile. Contact site administrator."), 'error');
      return $this->redirect('user.login');
    }

    // Get user's email from the FB profile.
    if (!$email = $this->fbManager->getEmail($fb_profile)) {
      drupal_set_message(t('Facebook login failed. This site requires permission to get your email address.'), 'error');
      return $this->redirect('user.login');
    }

    $facebook_id = $fb_profile->getField('id');
    // If we have an existing user with the same email address, try to log in.
    if ($drupal_user = $this->userManager->loadUserByProperty('facebook_id', $facebook_id)) {
      if ($this->userManager->loginUser($drupal_user)) {
        return new RedirectResponse($this->postLoginManager->getPostLoginPath());
      }
      else {
        return $this->redirect('user.login');
      }
    }

    // If there was no existing user, try to create a new user.
    $fields = [
      'email' => $email,
      'facebook_id' => $facebook_id,
    ];
    if ($drupal_user = $this->userManager->createUser($fb_profile->getField('name'), $fields)) {

      // Download profile picture for the newly created user.
      if ($picture_url = $this->fbManager->getFbProfilePicUrl()) {
        $this->userManager->setProfilePic($drupal_user, $picture_url, $fb_profile->getField('id'));
      }

      // Log the newly created user in.
      if ($this->userManager->loginUser($drupal_user)) {

        // Check if new users should be redirected to Drupal user form.
        if ($this->postLoginManager->getRedirectNewUsersToUserFormSetting()) {
          drupal_set_message(t("Please check your account details. Since you logged in with Facebook, you don't need to update your password."));
          return new RedirectResponse($this->postLoginManager->getPathToUserForm($drupal_user));
        }

        // Use normal post login path if user wasn't redirected to user form.
        return new RedirectResponse($this->postLoginManager->getPostLoginPath());
      }

      else {
        // New user was created but the account is pending approval.
        drupal_set_message(t('You will receive an email when site administrator activates your account.'), 'warning');
        return $this->redirect('user.login');
      }
    }

    else {
      // User could not be created.
      return $this->redirect('user.login');
    }

    // This should never be reached, user should have been redirected already.
    throw new AccessDeniedHttpException();
  }


}

In a service provicer, switch the user manager

    // Swap out facebook user manager.
    if ($container->hasDefinition('simple_fb_connect.user_manager')) {
      $definition = $container->getDefinition('simple_fb_connect.user_manager');
      $definition->setClass('Drupal\yourmodule\YourModuleFacebookConnectUserManager');
    }

That manager looks like this


namespace Drupal\yourmodule;

use Drupal\simple_fb_connect\SimpleFbConnectUserManager;
use Symfony\Component\EventDispatcher\GenericEvent;
use Drupal\Core\Entity\EntityStorageException;

/**
 * Contains all logic that is related to Drupal user management.
 */
class YourModuleFacebookConnectUserManager extends SimpleFbConnectUserManager {

  /**
   * Create a new user account.
   *
   * @param string $name
   *   User's name on Facebook.
   * @param array $fields
   *   $fields
   *
   * @return \Drupal\user\Entity\User|false
   *   Drupal user account if user was created
   *   False otherwise
   */
  public function createUser($name, $fields) {

    $email = isset($fields['email']) ? $fields['email'] : '';
    $facebook_id = isset($fields['facebook_id']) ? $fields['facebook_id'] : '';

    // Make sure we have everything we need.
    if (!$name || !$email || !$facebook_id) {
      $this->loggerFactory
        ->get('simple_fb_connect')
        ->error('Failed to create user. Name: @name, email: @email', array('@name' => $name, '@email' => $email));
      $this->drupalSetMessage($this->t('Error while creating user account. Please contact site administrator.'), 'error');
      return FALSE;
    }

    // Check if site configuration allows new users to register.
    if ($this->registrationBlocked()) {

      $this->loggerFactory
        ->get('simple_fb_connect')
        ->warning('Failed to create user. User registration is disabled in Drupal account settings. Name: @name, email: @email.', array('@name' => $name, '@email' => $email));

      $this->drupalSetMessage($this->t('Only existing users can log in with Facebook. Contact system administrator.'), 'error');
      return FALSE;
    }

    // Set up the user fields.
    // - Username will be user's name on Facebook.
    // - Password can be very long since the user doesn't see this.
    $fields = array(
      'name' => $this->generateUniqueUsername($name),
      'mail' => $email,
      'init' => $email,
      'pass' => $this->userPassword(32),
      'status' => $this->getNewUserStatus(),
      'facebook_id' => $facebook_id,
    );

    // Create new user account.
    $new_user = $this->entityTypeManager
      ->getStorage('user')
      ->create($fields);

    // Try to save the new user account.
    try {
      $new_user->save();

      $this->loggerFactory
        ->get('simple_fb_connect')
        ->notice('New user created. Username @username, UID: @uid', array('@username' => $new_user->getAccountName(), '@uid' => $new_user->id()));

      // Dispatch an event so that other modules can react to the user creation.
      // Set the account twice on the event: as the main subject but also in the
      // list of arguments.
      $event = new GenericEvent($new_user, ['account' => $new_user]);
      $this->eventDispatcher->dispatch('simple_fb_connect.user_created', $event);

      return $new_user;
    }

    catch (EntityStorageException $ex) {
      $this->drupalSetMessage($this->t('Creation of user account failed. Please contact site administrator.'), 'error');
      $this->loggerFactory
        ->get('simple_fb_connect')
        ->error('Could not create new user. Exception: @message', array('@message' => $ex->getMessage()));
    }

    return FALSE;
  }


}
masipila’s picture

It is much simpler to just add a listener to the FB login / FB user creation events and add your custom code there.

The module handbook has a working example for adding a role to the user. That example can be used as a basis for modifying other user fields.

https://www.drupal.org/node/2643016

Cheers,
Markus

swentel’s picture

@masipila not if you want to use the facebook id as the canonical to login, instead of email. We're missing an event there in the controller - or something else. Some pseudo code to explain the problem

    // Let someone else determine if the user can login or not
    $drupal_user = NULL;
    \Drupal::moduleHandler()->invokeAll('fb_simple_connect', [$drupal_user, $fb_profile]);

    // If we have an existing user with the same email address, try to log in.
    if (!$drupal_user) {
      if ($drupal_user = $this->userManager->loadUserByProperty('mail', $email)) {
        if ($this->userManager->loginUser($drupal_user)) {
          return new RedirectResponse($this->postLoginManager->getPostLoginPath());
        }
        else {
          return $this->redirect('user.login');
        }
     }
    }

Now, I probably don't need to take over the create user method, since I can probably just implement hook_user_presave(), but a hook there to alter the user object that you are creating before it's saved would be very handy as well.

That would save tons of overrides here :)

masipila’s picture

Hi,

Thanks for the clarification! The concept for dispatching a new symfony event before the user is saved is ok to me but please open a new issue for that purpose.

Patches are also more than welcome (to the new issue concentrating on the new event) because my own time is extremly tight at the moment. And let's use event dispatching instead of traditional hooks so that the event is also automatically available for Rules without any extra effort.

Cheers,
Markus