<?php /** * @copyright 2009-2019 Vanilla Forums Inc. * @license http://www.opensource.org/licenses/gpl-2.0.php GNU GPL v2 * @package Facebook */ use Vanilla\Web\CurlWrapper; /** * Class FacebookPlugin */ class FacebookPlugin extends SSOAddon { const API_VERSION = "2.7"; /** Authentication table key. */ const PROVIDER_KEY = "Facebook"; private const AUTHENTICATION_SCHEME = "facebook"; /** @var string */ protected $_AccessToken = null; /** @var null */ protected $_RedirectUri = null; /** @var SsoUtils */ private $ssoUtils; /** * Constructor. * * @param SsoUtils $ssoUtils */ public function __construct(SsoUtils $ssoUtils) { parent::__construct(); $this->ssoUtils = $ssoUtils; } /** * Get the AuthenticationSchemeAlias value. * * @return string The AuthenticationSchemeAlias. */ protected function getAuthenticationSchemeAlias(): string { return self::AUTHENTICATION_SCHEME; } /** * Retrieve an access token from the session. * * @return bool|mixed|null */ public function accessToken() { if (!$this->isConfigured()) { return false; } if ($this->_AccessToken === null) { if (Gdn::session()->isValid()) { $this->_AccessToken = valr(self::PROVIDER_KEY . ".AccessToken", Gdn::session()->User->Attributes); } else { $this->_AccessToken = false; } } return $this->_AccessToken; } /** * Redirect current user to the authorization URI. * * @param bool $query */ public function authorize($query = false) { $uri = $this->authorizeUri($query); redirectTo($uri, 302, false); } /** * Send a request to Facebook's API. * * @param string $path * @param bool $post * * @return string|array Response from the API. * @throws Gdn_UserException */ public function api($path, $post = false) { // Build the url. $url = "https://graph.facebook.com/v" . self::API_VERSION . "/" . ltrim($path, "/"); $accessToken = $this->accessToken(); if (!$accessToken) { throw new Gdn_UserException("You don't have a valid Facebook connection."); } if (strpos($url, "?") === false) { $url .= "?"; } else { $url .= "&"; } $url .= "access_token=" . urlencode($accessToken); $ch = curl_init(); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($ch, CURLOPT_URL, $url); if ($post !== false) { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $post); trace(" POST $url"); } else { trace(" GET $url"); } $response = CurlWrapper::curlExec($ch, false); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); curl_close($ch); Gdn::controller()->setJson("Type", $contentType); if (strpos($contentType, "javascript") !== false) { $result = json_decode($response, true); if (isset($result["error"])) { Gdn::dispatcher()->passData("FacebookResponse", $result); throw new Gdn_UserException($result["error"]["message"]); } } else { $result = $response; } return $result; } /** * Add Facebook button to normal signin page. * * @param Gdn_Controller $sender * @param array $args */ public function entryController_signIn_handler($sender, $args) { if (!$this->socialSignIn()) { return; } if (isset($sender->Data["Methods"])) { // pass the relative path, socialSigninButton handles the URL. $url = "/entry/facebook"; // Add the facebook method to the controller. $fbMethod = [ "Name" => self::PROVIDER_KEY, "SignInHtml" => socialSigninButton("Facebook", $url, "button"), ]; $sender->Data["Methods"][] = $fbMethod; } } /** * Add 'Facebook' option to the reactions row under posts. * * @param Gdn_Controller $sender * @param array $args */ public function base_afterReactions_handler($sender, $args) { if (!$this->socialReactions()) { return; } if (!is_array($args) || empty($args)) { return; } $recordType = $args["RecordType"] ?? null; if (!is_string($recordType)) { return; } if (strtolower($recordType) === "comment" && array_key_exists("Comment", $args)) { $url = commentUrl($args["Comment"]); } elseif (strtolower($recordType) === "discussion" && array_key_exists("Discussion", $args)) { $url = discussionUrl($args["Discussion"]); } else { return; } echo Gdn_Theme::bulletItem("Share"); $this->addReactButton($url); } /** * Output Quote link to share via Facebook. * * @param string $url */ private function addReactButton(string $url) { $query = http_build_query(["u" => $url]); $sharingUrl = "https://www.facebook.com/sharer/sharer.php?{$query}"; echo anchor( sprite("ReactFacebook", "Sprite ReactSprite", t("Share on Facebook")), url($sharingUrl, true), "ReactButton PopupWindow", ["rel" => "nofollow", "role" => "button"] ); } /** * Add Facebook button to MeModule. * * @param Gdn_Controller $sender * @param array $args */ public function base_signInIcons_handler($sender, $args) { if (!$this->socialSignIn()) { return; } echo "\n" . $this->_getButton(); } /** * Add Facebook button to GuestModule. * * @param Gdn_Controller $sender * @param array $args */ public function base_beforeSignInButton_handler($sender, $args) { if (!$this->socialSignIn()) { return; } echo "\n" . $this->_getButton(); } /** * Add Facebook button to default mobile theme. * * @param Gdn_Controller $sender */ public function base_beforeSignInLink_handler($sender) { if (!$this->socialSignIn()) { return; } if (!Gdn::session()->isValid()) { echo "\n" . wrap($this->_getButton(), "li", ["class" => "Connect FacebookConnect"]); } } /** * Make this available as an SSO method to users. * * @param Gdn_Controller $sender * @param array $args */ public function base_getConnections_handler($sender, $args) { $profile = valr("User.Attributes." . self::PROVIDER_KEY . ".Profile", $args); $sender->Data["Connections"][self::PROVIDER_KEY] = [ "Icon" => $this->getWebResource("icon.png", "/"), "Name" => "Facebook", "ProviderKey" => self::PROVIDER_KEY, "ConnectUrl" => $this->authorizeUri(false, self::profileConnectUrl()), "Profile" => [ "Name" => val("name", $profile), "Photo" => "//graph.facebook.com/{$profile["id"]}/picture?width=200&height=200", ], ]; } /** * Endpoint to handle connecting user to Facebook. * * @param ProfileController $sender * @param mixed $userReference * @param string $username * @param string|bool $code * * @throws Gdn_UserException */ public function profileController_facebookConnect_create($sender, $userReference, $username, $code = false) { $sender->permission("Garden.SignIn.Allow"); $state = json_decode(Gdn::request()->get("state", ""), true); $suppliedStateToken = val("token", $state); $this->ssoUtils->verifyStateToken("facebookSocial", $suppliedStateToken); $sender->getUserInfo($userReference, $username, "", true); $sender->_setBreadcrumbs(t("Connections"), "/profile/connections"); // Get the access token. $accessToken = $this->getAccessToken($code, self::profileConnectUrl()); // Get the profile. $profile = $this->getProfile($accessToken); // Save the authentication. Gdn::userModel()->saveAuthentication([ "UserID" => $sender->User->UserID, "Provider" => self::PROVIDER_KEY, "UniqueID" => $profile["id"], ]); // Save the information as attributes. $attributes = [ "AccessToken" => $accessToken, "Profile" => $profile, ]; Gdn::userModel()->saveAttribute($sender->User->UserID, self::PROVIDER_KEY, $attributes); $this->EventArguments["Provider"] = self::PROVIDER_KEY; $this->EventArguments["User"] = $sender->User; $this->fireEvent("AfterConnection"); redirectTo(self::profileConnectUrl()); } /** * Build-A-Button. * * @return string */ private function _getButton() { // pass the relative path, socialSigninButton handles the URL. $url = "/entry/facebook"; return socialSigninButton("Facebook", $url, "icon", ["rel" => "nofollow"]); } /** * Endpoint for configuring this addon. * * @param $sender * @param $args */ public function socialController_facebook_create($sender, $args) { $sender->permission("Garden.Settings.Manage"); if ($sender->Form->authenticatedPostBack()) { $settings = [ "Plugins.Facebook.ApplicationID" => trim($sender->Form->getFormValue("ApplicationID")), "Plugins.Facebook.Secret" => trim($sender->Form->getFormValue("Secret")), "Plugins.Facebook.UseFacebookNames" => $sender->Form->getFormValue("UseFacebookNames"), "Plugins.Facebook.SocialSignIn" => $sender->Form->getFormValue("SocialSignIn"), "Plugins.Facebook.SocialReactions" => $sender->Form->getFormValue("SocialReactions"), "Garden.Registration.SendConnectEmail" => $sender->Form->getFormValue("SendConnectEmail"), ]; saveToConfig($settings); $sender->informMessage(t("Your settings have been saved.")); } else { $sender->Form->setValue("ApplicationID", c("Plugins.Facebook.ApplicationID")); $sender->Form->setValue("Secret", c("Plugins.Facebook.Secret")); $sender->Form->setValue("UseFacebookNames", c("Plugins.Facebook.UseFacebookNames")); $sender->Form->setValue("SendConnectEmail", c("Garden.Registration.SendConnectEmail", false)); $sender->Form->setValue("SocialSignIn", c("Plugins.Facebook.SocialSignIn", true)); $sender->Form->setValue("SocialReactions", $this->socialReactions()); } $sender->setHighlightRoute("dashboard/social"); $sender->setData("Title", t("Facebook Settings")); $sender->render("Settings", "", "plugins/Facebook"); } /** * Standard SSO hook into Vanilla to handle authentication & user info transfer. * * @param Gdn_Controller $sender * @param array $args */ public function base_connectData_handler($sender, $args) { if (val(0, $args) != "facebook") { return; } $state = json_decode(Gdn::request()->get("state", ""), true); $suppliedStateToken = val("token", $state); $this->ssoUtils->verifyStateToken("facebook", $suppliedStateToken); if (isset($_GET["error"])) { // TODO global nope x2 throw new Gdn_UserException( val("error_description", $_GET, t("There was an error connecting to Facebook")) ); } $code = val("code", $_GET); // TODO nope $query = ""; if ($sender->Request->get("display")) { $query = "display=" . urlencode($sender->Request->get("display")); } $redirectUri = concatSep("&", $this->redirectUri(), $query); $accessToken = $sender->Form->getFormValue("AccessToken"); // Get the access token. if (!$accessToken && $code) { // Exchange the token for an access token. $code = urlencode($code); $accessToken = $this->getAccessToken($code, $redirectUri); $newToken = true; } // Get the profile. try { $profile = $this->getProfile($accessToken); } catch (Exception $ex) { if (!isset($newToken)) { // There was an error getting the profile, which probably means the saved access token is no longer valid. Try and reauthorize. if ($sender->deliveryType() == DELIVERY_TYPE_ALL) { redirectTo($this->authorizeUri(), 302, false); } else { $sender->setHeader("Content-type", "application/json"); $sender->deliveryMethod(DELIVERY_METHOD_JSON); $sender->setRedirectTo($this->authorizeUri(), false); } } else { $sender->Form->addError("There was an error with the Facebook connection."); } } // This isn't a trusted connection. Don't allow it to automatically connect a user account. saveToConfig("Garden.Registration.AutoConnect", false, false); $form = $sender->Form; //new gdn_Form(); $iD = val("id", $profile); $form->setFormValue("UniqueID", $iD); $form->setFormValue("Provider", self::PROVIDER_KEY); $form->setFormValue("ProviderName", "Facebook"); $form->setFormValue("FullName", val("name", $profile)); $form->setFormValue("Email", val("email", $profile)); $form->setFormValue("Photo", "//graph.facebook.com/{$iD}/picture?width=200&height=200"); $form->setFormValue("Target", val("target", $state, "/")); $form->addHidden("AccessToken", $accessToken); if (c("Plugins.Facebook.UseFacebookNames")) { $form->setFormValue("Name", val("name", $profile)); saveToConfig( [ "Garden.User.ValidationRegex" => UserModel::USERNAME_REGEX_MIN, "Garden.User.ValidationLength" => "{3,50}", "Garden.Registration.NameUnique" => false, ], "", false ); } // Save some original data in the attributes of the connection for later API calls. $attributes = []; $attributes[self::PROVIDER_KEY] = [ "AccessToken" => $accessToken, "Profile" => $profile, ]; $form->setFormValue("Attributes", $attributes); $sender->setData("Verified", true); } /** * Retrieve a Facebook access token. * * @param string $code * @param string $redirectUri * @param bool $throwError * * @return mixed * @throws Gdn_UserException */ protected function getAccessToken($code, $redirectUri, $throwError = true) { $get = [ "client_id" => c("Plugins.Facebook.ApplicationID"), "client_secret" => c("Plugins.Facebook.Secret"), "code" => $code, "redirect_uri" => $redirectUri, ]; $url = "https://graph.facebook.com/oauth/access_token?" . http_build_query($get); // Get the redirect URI. $c = curl_init(); curl_setopt($c, CURLOPT_RETURNTRANSFER, true); curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($c, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($c, CURLOPT_URL, $url); $contents = CurlWrapper::curlExec($c, false); $info = curl_getinfo($c); if (strpos(val("content_type", $info, ""), "/javascript") !== false) { $tokens = json_decode($contents, true); } elseif (strpos(val("content_type", $info, ""), "/json") !== false) { $tokens = json_decode($contents, true); } else { parse_str($contents, $tokens); } if (val("error", $tokens)) { throw new Gdn_UserException( "Facebook returned the following error: " . valr("error.message", $tokens, "Unknown error."), 400 ); } $accessToken = val("access_token", $tokens); return $accessToken; } /** * Get users' Facebook profile data. * * @param $accessToken * @return mixed */ public function getProfile($accessToken) { $url = "https://graph.facebook.com/me?access_token=$accessToken&fields=name,id,email"; $c = curl_init(); curl_setopt($c, CURLOPT_RETURNTRANSFER, true); curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($c, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS); curl_setopt($c, CURLOPT_URL, $url); $contents = CurlWrapper::curlExec($c, false); $profile = json_decode($contents, true); return $profile; } /** * Where to send authorization request. * * @param bool $query * @param bool $redirectUri * * @return string URL. */ public function authorizeUri($query = false, $redirectUri = false) { $appID = c("Plugins.Facebook.ApplicationID"); $fBScope = c("Plugins.Facebook.Scope", "email"); if (is_array($fBScope)) { $scopes = implode(",", $fBScope); } else { $scopes = $fBScope; } if (!$redirectUri) { $redirectUri = $this->redirectUri(); } if ($query) { $redirectUri .= stripos($redirectUri, "?") === false ? "?" : "&" . $query; } // Get a state token. $stateToken = $this->ssoUtils->getStateToken(); $authQuery = http_build_query([ "client_id" => $appID, "redirect_uri" => $redirectUri, "scope" => $scopes, "state" => json_encode(["token" => $stateToken, "target" => $this->getTargetUri()]), ]); $signinHref = "https://graph.facebook.com/oauth/authorize?{$authQuery}"; if ($query) { $signinHref .= "&" . $query; } return $signinHref; } /** * Send the Facebook entry page to Facebook as the redirectURI. * * @param null $newValue * * @return null|string URL. */ public function redirectUri($newValue = null) { if ($newValue !== null) { $this->_RedirectUri = $newValue; } elseif ($this->_RedirectUri === null) { $redirectUri = url("/entry/connect/facebook", true); if (strpos($redirectUri, "=") !== false) { $p = strrchr($redirectUri, "="); $uri = substr($redirectUri, 0, -strlen($p)); $p = urlencode(ltrim($p, "=")); $redirectUri = $uri . "=" . $p; } $this->_RedirectUri = $redirectUri; } return $this->_RedirectUri; } /** * Get the target URL to pass to the state when making requests. * * @return mixed|string */ public function getTargetUri() { $target = Gdn::request()->getValueFrom(Gdn_Request::INPUT_GET, "Target", Gdn::request()->getPath()); if (ltrim($target, "/") == "entry/signin" || ltrim($target, "/") == "entry/facebook") { $target = "/"; } return $target; } /** * Get the URL for connection page. * * @return string URL. */ public static function profileConnectUrl() { return url("entry/connect/facebook", true); } /** * Whether this plugin is setup with enough info to function. * * @return bool */ public function isConfigured() { $appID = c("Plugins.Facebook.ApplicationID"); $secret = c("Plugins.Facebook.Secret"); if (!$appID || !$secret) { return false; } return true; } /** * Whether social signin is enabled. * * @return bool */ public function socialSignIn() { return c("Plugins.Facebook.SocialSignIn", true) && $this->isConfigured(); } /** * Whether social reactions is enabled. * * @return bool */ public function socialReactions(): bool { return (bool) c("Plugins.Facebook.SocialReactions", true); } /** * Create an entry/facebook endpoint that redirects to the authorization URI. */ public function entryController_facebook_create() { redirectTo($this->authorizeUri(), 302, false); } /** * Run once on enable. * * @throws Gdn_UserException */ public function setup() { $error = ""; if (!function_exists("curl_init")) { $error = concatSep("\n", $error, "This plugin requires curl."); } if ($error) { throw new Gdn_UserException($error, 400); } $this->structure(); } /** * Run on utility/update. */ public function structure() { // Save the facebook provider type. Gdn::sql()->replace( "UserAuthenticationProvider", [ "AuthenticationSchemeAlias" => self::AUTHENTICATION_SCHEME, "URL" => "...", "AssociationSecret" => "...", "AssociationHashMethod" => "...", ], ["AuthenticationKey" => self::PROVIDER_KEY], true ); } }