<?php
/*
Plugin Name: Facebook Login
Description: Extends Website Membership to support login and sign-up via Facebook
Version: 2.01
Requires at least: 3.10
*/

// DON'T UPDATE ANYTHING BELOW THIS LINE

/* DEVELOPER REFERENCE:
  Facebook Login:            https://developers.facebook.com/docs/facebook-login/web
  Facebook Graph API:        https://developers.facebook.com/docs/graph-api/overview
  Facebook Javascript SDK:   https://developers.facebook.com/docs/javascript
  Facebook PHP SDK:          https://developers.facebook.com/docs/reference/php
  Facebook PHP SDK (github): https://github.com/facebook/php-graph-sdk
*/

require_once("facebookLogin_utils.php");

// plugin actions
addAction('section_init',                         '_fbl_getPluginErrors', null, 1); // show errors on plugin menu
addAction('_websiteLogin_init.pre_actionHandler', '_fbl_installPlugin', null, -1);
addAction('_websiteLogin_init.pre_actionHandler', 'fbl_login', null, 0); // called by plugin hook in _websiteLogin_init()

// DON'T UPDATE ANYTHING BELOW THIS LINE
$GLOBALS['FACEBOOKAPI_ENABLE_LOGIN'] = true; // Show login blocks generated with earlier versions of plugin (v1.05 and earlier)

// Get/set plugin settings
// $username = fbl_settings('username');
// fbl_settings('api_version', '2.9');
function fbl_settings($key, $value = null) {

  // load config file
  $fileLoaded = false;
  if (!$fileLoaded) {
    $configPath = __DIR__ .'/'. "facebookLogin_settings.php";
    if (!file_exists($configPath)) { die("Can't find file '" .basename($configPath). "', see readme.txt for setup instructions."); }
    require_once($configPath);
    $fileLoaded = true;
  }

  // settings
  static $settings = [];
  if (!$settings) {  // set initial values.  *** Don't change these values, set values in facebookLogin_settings.php
    $settings['app_id']         = '';
    $settings['app_secret']     = '';
    $settings['api_version']    = 'v2.9';
    $settings['debug']          = false;   // FOR DEBUGGING ONLY - DO NOT USE IN PRODUCTION

    // advanced settings
    $settings['fb_sdk_url']     = '//connect.facebook.net/en_US/sdk.js';    // language and locale code: https://developers.facebook.com/docs/internationalization#locales
    $settings['fb_fields']      = 'email,name,first_name,last_name';        // fields returned by login: https://developers.facebook.com/docs/reference/api/user/
    $settings['fb_permissions'] = 'public_profile,email';                   // permissions requested: https://developers.facebook.com/docs/facebook-login/permissions
  }

  // set value for keys
  if (!is_null($value)) {
    if (!isset($settings[$key])) { dieAsCaller(__FUNCTION__ .": Unknown key '" .htmlencode($key). "'!"); } // don't allow unknown keys
    $settings[$key] = $value;
  }

  // error checking
  if (!array_key_exists($key, $settings)) {
    dieAsCaller(__FUNCTION__ .": Unknown key '" .htmlencode($key). "'!");
  }

  //
  return $settings[$key];
}



// Output javascript code for facebook login form
/* Usage:
  <?php // Facebook Login - If plugin enabled, output Facebook Login javascript
    if (function_exists('fbl_login')) {  fbl_loginForm_javascript(); }
  ?>
*/
function fbl_loginForm_javascript() {

  // for mobile apps using cordova and webview components - use cordova specific functions
  $isCordova = isset($_SERVER['HTTP_USER_AGENT']) && contains('Cordova', $_SERVER['HTTP_USER_AGENT']); // Mobile App
  if ($isCordova) { return _fbl_loginForm_javascript_cordova(); }

  // for browsers
?>
<!-- Start: Facebook Login Code ............................................................... -->
<script>
  // Load the Facebook SDK asynchronously
  (function(d, s, id){
    var js, fjs = d.getElementsByTagName(s)[0];
    if (d.getElementById(id)) {return;}
    js = d.createElement(s); js.id = id;
    js.src = "<?php echo fbl_settings('fb_sdk_url'); ?>";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));

  // Facebook SDK init options
  window.fbAsyncInit = function() {
   FB.init({ // Reference: https://developers.facebook.com/docs/javascript/reference/FB.init/
     appId   : '<?php echo fbl_settings('app_id'); ?>',
     version : '<?php echo fbl_settings('api_version'); ?>',
     //xfbml   : true
   });
  };

  // Facebook Login - This function is called when the user clicks the "Login with Facebook" button.  It gets their
  // ... accessToken and redirect them to ?action=fbLogin&accessToken=xxxx which calls fbl_login.
  function fbl_login() {

    // get access token and redirect to login url
    fbl_login_getAccessToken(function(accessToken) { // success
      window.location = "?action=fbLogin&accessToken=" + encodeURIComponent(accessToken);  // on valid facebook login, redirect to wsm facebook login url
    },
    function(errorCode, errorText) { // error
      console.log(errorCode, errorText);
    });

    return false;
  }

  // This function is called when the user clicks the "Connect Facebook" button on their edit profile page.  It gets their
  // ... accessToken and redirect them to ?action=fbLogin_connect&accessToken=xxxx which calls fbl_login.
  <?php // be sure to update this function in both _fbl_loginForm_javascript and _fbl_loginForm_javascript_cordova ?>
  function fbl_login_connect() {

    // get access token and redirect to login url
    fbl_login_getAccessToken(function(accessToken) { // success
      window.location = "?action=fbLogin_connect&accessToken=" + encodeURIComponent(accessToken);  // on valid facebook login, redirect to wsm facebook login url
    },
    function(errorCode, errorText) { // error
      console.log(errorCode, errorText);
    });

    return false;
  }

  // This function is called when the user clicks the "Disconnect Facebook" button on their edit profile page.  It redirects them to
  // ... ?action=fbLogin_disconnect which calls fbl_login.
  <?php // be sure to update this function in both _fbl_loginForm_javascript and _fbl_loginForm_javascript_cordova ?>
  function fbl_login_disconnect() {
    window.location = "?action=fbLogin_disconnect";
  }


  // Prompt user to authorize facebook if needed and return accessToken.  On failure returns nothing
  function fbl_login_getAccessToken(successCallback, errorCallback) {

    //
    FB.login(function(response) {
      if (response.status !== 'connected')    { return errorCallback(100, "No authorization"); } // do nothing if they didn't authorize login
      if (!response.authResponse)             { return errorCallback(200, "No authResponse"); }   // do nothing if we didn't get an authResponse object
      if (!response.authResponse.accessToken) { return errorCallback(300, "No accessToken"); }    // do nothing if no access token

      // check if we were granted email permission. This would be set to declined if user manually unselected
      // email when authorizing with facebook.  Do nothing if not, next login click will rerequest permission
      var grantedScopes = response.authResponse.grantedScopes;
      if (!grantedScopes.match(/\b(email)\b/)) { return errorCallback(400, "No email grant"); } // scopes eg: email,public_profile

      <?php /*
      // Secondary method - Not needed is grantedScopes is available above.
      // check if we were granted email permission. This would be set to declined if user manually unselected
      // email when authorizing with facebook.  Do nothing if not, next login click will rerequest permission
      FB.api('/me/permissions', function(response) {
        var permissions        = []; // permission names to values, eg: [email: "declined", public_profile: "granted"]
        for (i = 0; i < response.data.length; i++) { permissions[ response.data[i].permission ] = response.data[i].status; }

        // redirect if user granted email permission.  Otherwise, do nothing.  When the user clicks login link again the
        // auth_type: 'rerequest' setting with cause non-granted permissions to be requested again.
        if (permissions['email'] == 'granted') {
          // NOTE: If we do this here don't do it below.
          // on valid facebook login, redirect to wsm facebook login url
          window.location = "?action=fbLogin&accessToken=" + encodeURIComponent(response.authResponse.accessToken);
        }
      });
      */ ?>

      // debug
      //console.log(response);

      //
      return successCallback(response.authResponse.accessToken);
    }, {
      scope: '<?php echo fbl_settings('fb_permissions'); ?>',
      auth_type: 'rerequest', // ask users for any previously declined permissions (such as if they uncheck "provide email" when authorizing via facebook)
      return_scopes: true
    });
    return false;
  }
</script>
<!-- End: Facebook Login Code ............................................................... -->
<?php
}

// facebook login javascript for mobile apps using cordova and webview components
// This is called by fbl_loginForm_javascript
function _fbl_loginForm_javascript_cordova() {
  ?>
  <script> // Facebook Login Function
    function fbl_login() {
      // For Cordova Apps/Browsers - Call RPC Facebook Login function and redirect to server login url on success
      //if (typeof(cordovaRPC) && navigator.userAgent.match(/Cordova/i)) { }
      cordovaRPC('facebookLogin', {}, function(accessToken) {
         window.location = '?action=fbLogin&accessToken=' + encodeURIComponent(accessToken);  // on success redirect to server login url
      });
      return false;
    }

  // This function is called when the user clicks the "Connect Facebook" button on their edit profile page.  It gets their
  // ... accessToken and redirect them to ?action=fbLogin_connect&accessToken=xxxx which calls fbl_login.
  <?php // be sure to update this function in both _fbl_loginForm_javascript and _fbl_loginForm_javascript_cordova ?>
  function fbl_login_connect() {
    cordovaRPC('facebookLogin', {}, function(accessToken) { // success
      window.location = "?action=fbLogin_connect&accessToken=" + encodeURIComponent(accessToken);  // on valid facebook login, redirect to wsm facebook login url
    });
    return false;
  }

  // This function is called when the user clicks the "Disconnect Facebook" button on their edit profile page.  It redirects them to
  // ... ?action=fbLogin_disconnect which calls fbl_login.
  <?php // be sure to update this function in both _fbl_loginForm_javascript and _fbl_loginForm_javascript_cordova ?>
  function fbl_login_disconnect() {
    window.location = "?action=fbLogin_disconnect";
  }
  </script>
 <?php
}



// Login Via Facebook - Authenticate with facebook by calling javascript fbl_login() function, then send a facebook
// accessToken to our script where we confirm facebook login is valid and then either lookup the local user with
// that Facebook ID and log them in as that user, or create a new user with their facebook info.
// Note: This function is called by plugin hook in _websiteLogin_init().
function fbl_login() {
  $validActions  = ['fbLogin','fbLogin_connect','fbLogin_disconnect'];
  $isValidAction = !empty($_REQUEST['action']) && in_array($_REQUEST['action'], $validActions);
  if (!$isValidAction) { return; } // skip unknown or unsupport actions

  // 
  if ($_REQUEST['action'] == 'fbLogin_disconnect') { return _fbl_login_disconnect(); }
  if (empty($_REQUEST['accessToken']))             { return; } // accessToken is required for fbLogin and fbLogin_connect

  // validate accessToken
  $shortLivedToken = $_REQUEST['accessToken'];
  $response        = fbg_isValidToken($shortLivedToken, $errors) or die("Invalid token: $errors");

  // Upgrade token - regular/short-term tokens last ~1-2 hours, long-term tokens last ~60 days
  $longLivedToken = fbg_upgradeToken($shortLivedToken, $errors) or die("Error upgrading token: $errors");

  // check permissions include: email
  $debugToken = fbg_debugToken( $longLivedToken, $errors ) or die("debug token errors: $errors");
  if (!in_array('email', $debugToken['data']['scopes'])) { die(t("Error: No facebook email found, please try again.")); }

  // get user fields.  At the minimum we need: facebook_id, email
  $fbData = fb_graph('/me', [
               // available fields with basic permissions: https://developers.facebook.com/docs/facebook-login/permissions/#reference-public_profile
              'fields' => 'email,name,first_name,middle_name,last_name,gender,id,link,updated_time,locale,timezone,picture.width(1000).height(1000),cover',  // available fields with basic permissions: https://developers.facebook.com/docs/facebook-login/permissions/#reference-public_profile
              // .type(large) or .width(500).height(500) uses field expansion: https://developers.facebook.com/docs/graph-api/using-graph-api/#fieldexpansion
              // ... and are documented here as /user/picture parameters: https://developers.facebook.com/docs/graph-api/reference/user/picture/
              // NOTE: Facebook CDN image urls EXPIRE after a few days, don't rely on them not changing: https://stackoverflow.com/questions/30477877/facebook-image-url-gets-expired
              //'metadata'     => 1, // return list of all available fields in response
              'access_token' => $longLivedToken
            ], $errors) or die("Errors: $errors");
  if (empty($fbData['id']))    { die("Error: No facebook ID in response!"); }
  if (empty($fbData['email'])) { die("Error: No facebook email in response!"); }

  // debug
  if (fbl_settings('debug')) { showme($fbData); }
  
  // List of fields to update in user account
  $colsToValues = [];
  $colsToValues['facebook_id']           = $fbData['id'];
  $colsToValues['facebook_access_token'] = $longLivedToken;
  
  // adding available information to the new user record.  If a CMS user field exists called facebook_*
  // ... matching a fieldbook field it will be populated.  Eg: creating a CMS field called facebook_gender will
  // ... cause that field to be populated on Facebook login.  a list of available fields can be found here: https://developers.facebook.com/docs/graph-api/reference/user
  // some fields require additional permissions be requested: https://developers.facebook.com/docs/facebook-login/permissions/
  // These fields are available by default: https://developers.facebook.com/docs/facebook-login/permissions/#reference-public_profile
  $schema = getSchemaFields(accountsTable());
  foreach ($fbData as $key => $value) {
    if (!array_key_exists("facebook_$key", $schema)) { continue; } // skip if no field
    $colsToValues["facebook_$key"] = $value;    
  }
  if (array_key_exists("facebook_debug", $schema)) { // add this debug fields to log everything returned by Graph API
    $colsToValues["facebook_debug"] = json_encode($fbData, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);    
  }
  
  // Connect Facebook for already logged in users
  if ($_REQUEST['action'] == 'fbLogin_connect') {
    _fbl_login_connect($colsToValues);
    return; 
  }

  // Update (or create) user account: On cms/web user matching facebook email OR Id - set facebook ID and access token
  // NOTE: If email already exists with no facebook id this effectively adds facebook login to existing account with matching email
  // If no matching email or facebook ID is found then a user account is created.
  $where = mysql_escapef("(facebook_id = ? OR email = ?)", $fbData['id'], $fbData['email']);
  $user  = mysql_get(accountsTable(), null, $where);
  if ($user) {
    mysql_update(accountsTable(), $user['num'], null, $colsToValues);
    if (fbl_settings('debug')) { print("(DEBUG) Updated user #{$user['num']} with:"); showme($colsToValues); }
  }
  else {
    $user = _fbl_login_createUser( $colsToValues, $fbData );
    if (fbl_settings('debug')) { print("(DEBUG) Created user #{$user['num']}:"); showme($user); }
  }

  // emulate login
  websiteLogin_setLoginTo($user['username'], $user['password']);
  $GLOBALS['CURRENT_USER'] = _websiteLogin_getCurrentUser();
  doAction('wsm_loginSuccess');

  // redirect on success
  $postLoginUrl = coalesce( getPrefixedCookie('lastUrl'), @$GLOBALS['WEBSITE_LOGIN_POST_LOGIN_URL'], '/' );
  if (fbl_settings('debug')) { die("(DEBUG) Redirect to: $postLoginUrl"); }
  redirectBrowserToURL($postLoginUrl);
  exit;
}

// add facebook credentials for the currently logged in user
// Note: This function is called by fbl_login()
function _fbl_login_connect($fbColsToValues) {
  global $CURRENT_USER;
  if (empty($CURRENT_USER['num'])) { return; } // require currently logged in user

  // clear this facebook id from any other users that have it (to prevent multiple users with the same facebook id)
  $where             = mysql_escapef(" num != ? AND facebook_id = ?", $CURRENT_USER['num'], $fbColsToValues['facebook_id']);
  $resetColsToValues = [ 'facebook_id' => '', 'facebook_access_token' => '' ];
  mysql_update(accountsTable(), null, $where, $resetColsToValues);

  // update facebook fields
  mysql_update(accountsTable(), $CURRENT_USER['num'], null, $fbColsToValues);
  redirectBrowserToURL('?'); // we need to either blank out $CURRENT_USER['facebook_id'], etc or reload page
}

// remove facebook credentials from the currently logged in user
// Note: This function is called by fbl_login()
function _fbl_login_disconnect() {
  global $CURRENT_USER;
  if (empty($CURRENT_USER['num'])) { return; } // require currently logged in user

  // Future: should we automatically deauthorize the app form the users account?
  // Ref: https://stackoverflow.com/questions/4037841/removing-facebook-application-through-api

  // remove stored facebook credentials
  $colsToValues = [ 'facebook_id' => '', 'facebook_access_token' => '' ];
  mysql_update(accountsTable(), $CURRENT_USER['num'], null, $colsToValues);
  redirectBrowserToURL('?'); // we need to either blank out $CURRENT_USER['facebook_id'], etc or reload page
}

// create and return a local user account based on facebook information
// Note: This function is called by fbl_login
function _fbl_login_createUser($colsToValues, $fbData) {

  // create password and email
  $passwordText = wsm_generatePassword();
  $email = $fbData['email'];
  
  // create new user fields
  $colsToValues = $colsToValues + [        // This adds these to any previously set colsToValues
    'createdDate='           => 'NOW()',
    'updatedDate='           => 'NOW()',
    'createdByUserNum'       => 0,
    'updatedByUserNum'       => 0,
    'fullname'               => $fbData['name'],
    'email'                  => $fbData['email'],
    'username'               => $fbData['email'],
    'password'               =>  getPasswordDigest( $passwordText ),
  ];
  
  // add user account
  $userNum = mysql_insert(accountsTable(), $colsToValues, true);
  $user    = mysql_get(accountsTable(), $userNum);
  
  // send signup email
  if ($email) { // email should always be set, but we check for it just in case
    list($mailErrors, $fromEmail) = wsm_sendSignupEmail($user['num'], $passwordText);
    if ($mailErrors) { dieAsCaller("Mail Error: $mailErrors"); }
  }

  //
  return $user;
}


// Import facebook profile photo into the current users photo field and remove old photos
// This function is intended to be used with custom code
// Usage: $errorsAndAlerts .= fbl_importProfilePicture('photo');
function _fbl_importProfilePicture($uploadField) {
  global $CURRENT_USER;
  if (empty($CURRENT_USER['num']))                    { return "You must be logged in to import profile photos!"; }
  if (empty($GLOBALS['CURRENT_USER']['facebook_id'])) { return "You must connect your facebook account before you can import your profile photo!"; }

  // Get profile photo url
  // Reference: https://developers.facebook.com/docs/graph-api/reference/user/picture/
  $facebookId = $GLOBALS['CURRENT_USER']['facebook_id'];
  // $facebookId = '100007205373238'; // debug, this is a silhouette only
  $response   = fb_graph("/$facebookId/picture", [
                  'redirect'  => 'false',
                  //'type'      => 'large', // large, square
                  'height'    => '800',  // request minimum 1000px width but no height to maintain aspect ratio
                  'width'     => '800',  // request minimum 1000px width but no height to maintain aspect ratio
                //'access_token' => _fbg_appAccessToken(),
                //'access_token' => $GLOBALS['CURRENT_USER']['facebook_access_token'],
                ], $errors) or die("Facebook Login Errors: $errors");
  $photoUrl     = $response['data']['url'];
  $isSilhouette = $response['data']['is_silhouette'];
  if (!$photoUrl) { trigger_error("Couldn't get facebook profile photo url!", E_USER_ERROR); }

  // debug
  //print "<p><img src='{$response['data']['url']}'></p>";

  // download photo as temp file
  $response = curl_get($photoUrl, null, $requestInfo);
  if ($requestInfo['error'])              { trigger_error("Error loading photo: {$requestInfo['error']}", E_USER_ERROR); }
  if ($requestInfo['http_code'] != '200') { trigger_error("Unexpected server response (HTTP {$requestInfo['http_code']})", E_USER_ERROR); }
  $tmpFilepath = tempnam(DATA_DIR, 'temp_facebook_profile_') . '.jpg';
  file_put_contents($tmpFilepath, $response);

  // remove previous profile photos
  removeUploads(mysql_where([
    'tableName' => accountsTable(),
    'fieldName' => $uploadField,
    'recordNum' => $CURRENT_USER['num'],
  ]));

  // upload temp photo to CMS and then remove
  $errors = saveUploadFromFilepath(accountsTable(), $uploadField, $CURRENT_USER['num'], null, $tmpFilepath);
  unlink($tmpFilepath); // remove temp file
  if ($errors) { trigger_error("Error saving photo: $errors", E_USER_ERROR); }

  return '';
}


// show plugin config errors.
// When called from the CMS plugin menu errors are shown in alert at top of page.
// When called from anywhere else errors are returned.
function _fbl_getPluginErrors() {
  $settingsFile = "facebookLogin_settings.php"; 
  $settingsPath = __DIR__ ."/$settingsFile";
  
  // error checking
  $errors      = '';
  $errorsArray = [];
  if     (!isset($GLOBALS['WEBSITE_MEMBERSHIP_PLUGIN']))           { $errorsArray[] = t("Website Membership plugin must be enabled!"); }
  elseif (!isset($GLOBALS['WEBSITE_MEMBERSHIP_VERSION']))          { $errorsArray[] = t("Website Membership plugin must be enabled!"); }
  elseif ($GLOBALS['WEBSITE_MEMBERSHIP_VERSION'] < 1.09)           { $errorsArray[] = t("Website Membership plugin must be upgraded to 1.09 or newer!"); }

  if (!file_exists($settingsPath))                                 { $errorsArray[] = t("Settings file $settingsFile not found! See readme.txt to setup plugin!"); }
  else { 
    if (!fbl_settings('app_id'))                                   { $errorsArray[] = t("You must enter a value for 'app_id' in $settingsFile!"); }
    if (!fbl_settings('app_secret'))                               { $errorsArray[] = t("You must enter a value for 'app_secret' in $settingsFile!"); }
  }
  if ($errorsArray) { $errors = t("Facebook Login: ") . implode("<br/>\n", $errorsArray) . "<br/>\n"; }

  // Plugins Menu: Show errors in alert at top of page
  $isPluginMenu = defined('IS_CMS_ADMIN') && !empty($_REQUEST['action']) && $_REQUEST['action'] == 'plugins';
  if ($isPluginMenu) { alert($errors); }

  // Anywhere else: Return errors as text (not currently used)
  if (!$isPluginMenu) { return $errors; }
}


// Add facebook fields to account table
function _fbl_installPlugin() {

  $schemaName   = accountsTable();
  $pluginFields = [
    'facebook_separator'    => [ 'separatorHeader' => 'Facebook Login', 'type' => 'separator', 'separatorType' => 'header bar' ],
    'facebook_id'           => [ 'label' => 'Facebook ID',           'type' => 'textfield', 'fieldWidth' => 'small', 'isUnique' => 1 ],
    'facebook_name'         => [ 'label' => 'Facebook Name',         'type' => 'textfield', 'fieldWidth' => 'small', 'isUnique' => 0 ],
    'facebook_gender'       => [ 'label' => 'Facebook Gender',       'type' => 'textfield', 'fieldWidth' => 'small', 'isUnique' => 0 ],
    'facebook_access_token' => [ 'label' => 'Facebook Access Token', 'type' => 'textfield', 'fieldWidth' => 'full',  'isUnique' => 1 ],
    //'facebook_debug'        => [ 'label' => 'Facebook Debug',        'type' => 'textbox', 'fieldHeight' => '300', 'autoFormat' => 0, ], // if defined this field will get the full response from the Graph API with all the user fields
  ];
  
  // add plugin fields if they don't exist
  $schema       = loadSchema($schemaName, null, true);
  $fieldsToAdd  = array_diff(array_keys($pluginFields), array_keys($schema));
  if ($fieldsToAdd) {
    $fieldOrder = time();
    foreach ($fieldsToAdd as $field) {
      $schema[$field] = $pluginFields[$field] + ['order' => $fieldOrder++];
    }
    saveSchema($schemaName, $schema);
    createMissingSchemaTablesAndFields();
  }
}

// eof