diff --git a/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-21.sql b/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-21.sql new file mode 100644 index 0000000000000..066a594341e12 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-21.sql @@ -0,0 +1,2 @@ +INSERT INTO `#__extensions` (`extension_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `system_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES +(458, 'plg_authentication_facebook', 'plugin', 'facebook', 'authentication', 0, 0, 1, 1, '', '', '', '', 0, '0000-00-00 00:00:00', 3, 0); diff --git a/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-21.sql b/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-21.sql new file mode 100644 index 0000000000000..a32d0a3263917 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-21.sql @@ -0,0 +1,2 @@ +INSERT INTO "#__extensions" ("extension_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES +(458, 'plg_authentication_facebook', 'plugin', 'facebook', 'authentication', 0, 1, 1, 1, '', '', '', '', 0, '1970-01-01 00:00:00', 3, 0); diff --git a/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-21.sql b/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-21.sql new file mode 100644 index 0000000000000..d4e9675081502 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-21.sql @@ -0,0 +1,6 @@ +SET IDENTITY_INSERT #__extensions ON; + +INSERT INTO #__extensions ([extension_id], [name], [type], [element], [folder], [client_id], [enabled], [access], [protected], [manifest_cache], [params], [custom_data], [system_data], [checked_out], [checked_out_time], [ordering], [state]) +SELECT 458, 'plg_authentication_facebook', 'plugin', 'facebook', 'authentication', 0, 1, 1, 1, '', '', '', '', 0, '1900-01-01 00:00:00', 3, 0; + +SET IDENTITY_INSERT #__extensions OFF; \ No newline at end of file diff --git a/administrator/components/com_users/helpers/users.php b/administrator/components/com_users/helpers/users.php index c3a9770ca39c7..0928de477cb25 100644 --- a/administrator/components/com_users/helpers/users.php +++ b/administrator/components/com_users/helpers/users.php @@ -174,8 +174,8 @@ public static function getRangeOptions() */ public static function getTwoFactorMethods() { - FOFPlatform::getInstance()->importPlugin('twofactorauth'); - $identities = FOFPlatform::getInstance()->runPlugins('onUserTwofactorIdentify', array()); + JPluginHelper::importPlugin('twofactorauth'); + $identities = JEventDispatcher::getInstance()->trigger('onUserTwofactorIdentify', array()); $options = array( JHtml::_('select.option', 'none', JText::_('JGLOBAL_OTPMETHOD_NONE'), 'value', 'text'), diff --git a/administrator/language/en-GB/en-GB.plg_authentication_facebook.ini b/administrator/language/en-GB/en-GB.plg_authentication_facebook.ini new file mode 100644 index 0000000000000..3537b2fdf9797 --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_authentication_facebook.ini @@ -0,0 +1,28 @@ +; Joomla! Project +; Copyright (C) 2005 - 2016 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +; Note 2: Do NOT alpha-sort the language keys. It makes translation far harder because it deprives the translator +; of the string's context! + +PLG_AUTHENTICATION_FACEBOOK="Authentication - Facebook" +PLG_AUTHENTICATION_FACEBOOK_XML_DESCRIPTION="Handles User Authentication with a Facebook account (Requires cURL).
You need to create a Facebook app for your site. Facebook login only works in the front-end of your site.
Warning!Facebook login bypasses Two Factor Authentication.
Warning! You must have at least one authentication plugin enabled or you will lose all access to your site." + +PLG_AUTHENTICATION_FACEBOOK_FIELD_APPID_LABEL="Facebook Application ID" +PLG_AUTHENTICATION_FACEBOOK_FIELD_APPID_DESC="Enter the App ID for your custom Facebook application here. This is required to enable social login with Facebook. You can create a Facebook app at https://developers.facebook.com/apps." +PLG_AUTHENTICATION_FACEBOOK_FIELD_APPSECRET_LABEL="Facebook Application Secret" +PLG_AUTHENTICATION_FACEBOOK_FIELD_APPSECRET_DESC="Enter the App Secret for your custom Facebook application here. This is required to enable social login with Facebook. You can create a Facebook app at https://developers.facebook.com/apps." +PLG_AUTHENTICATION_FACEBOOK_FIELD_CREATENEW_LABEL="Create new user accounts?" +PLG_AUTHENTICATION_FACEBOOK_FIELD_CREATENEW_DESC="Creates a new Joomla! user when a user tries to log in via Facebook but there is no Joomla! user account associated with that e-mail or Facebook User ID. If user registration is disabled no account will be created and an error will be raised. The Joomla! user has a username derived from the Facebook login, the same email address as the Facebook account and a long, random password (which the user can change once they have logged in). Set this to No to prevent creation of user accounts through Facebook login." + +PLG_AUTHENTICATION_FACEBOOK_BTN_LABEL="Facebook Login" + +PLG_AUTHENTICATION_FACEBOOK_ERROR_NOT_LOGGED_IN_FB="You are not logged into Facebook or you didn't grant permissions for our site to log you in through Facebook" +PLG_AUTHENTICATION_FACEBOOK_ERROR_ACCOUNT_DISABLED_OR_NOT_ACTIVATED="Your account on our site is disabled or not activated." +PLG_AUTHENTICATION_FACEBOOK_ERROR_LOCAL_NOT_FOUND="You do not have an account on our site that corresponds to this Facebook account." +PLG_AUTHENTICATION_FACEBOOK_ERROR_LOCAL_USERNAME_CONFLICT="An account with the same username as your Facebook account already exists on our site. Please create a new user account on our site and then link your Facebook account to it." +PLG_AUTHENTICATION_FACEBOOK_ERROR_CANNOT_CREATE="Could not create a new user on our site: %s" + +PLG_AUTHENTICATION_FACEBOOK_NOTICE_USERACTIVATE="Your email address needs to be verified before logging in to our site. An email has been sent to you with instructions. After following these instructions please come back here and retry logging in to our site using Facebook." +PLG_AUTHENTICATION_FACEBOOK_NOTICE_ADMINACTIVATE="Your account needs to be reviewed by an administrator. When the review process is complete you will receive a confirmation email. After receiving that email please come back here and retry logging in to our site using Facebook." diff --git a/administrator/language/en-GB/en-GB.plg_authentication_facebook.sys.ini b/administrator/language/en-GB/en-GB.plg_authentication_facebook.sys.ini new file mode 100644 index 0000000000000..e4066d3827b8c --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_authentication_facebook.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; Copyright (C) 2005 - 2016 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +PLG_AUTHENTICATION_FACEBOOK="Authentication - Facebook" +PLG_AUTHENTICATION_FACEBOOK_XML_DESCRIPTION="Handles User Authentication with a Facebook account (Requires cURL).
You need to create a Facebook app for your site.
Warning!Facebook login bypasses Two Factor Authentication.
Warning! You must have at least one authentication plugin enabled or you will lose all access to your site." diff --git a/administrator/manifests/packages/pkg_weblinks.xml b/administrator/manifests/packages/pkg_weblinks.xml deleted file mode 100644 index f8699dcaaecd2..0000000000000 --- a/administrator/manifests/packages/pkg_weblinks.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - pkg_weblinks - weblinks - December 2012 - Joomla! Project - (C) 2005 - 2016 Open Source Matters. All rights reserved. - admin@joomla.org - www.joomla.org - Joomla! Project - admin@joomla.org - www.joomla.org - 3.4.1 - GNU General Public License version 2 or later; see LICENSE.txt - PKG_WEBLINKS_XML_DESCRIPTION - - - com_weblinks.zip - mod_weblinks.zip - plg_finder_weblinks.zip - plg_search_weblinks.zip - - - en-GB/en-GB.pkg_weblinks.sys.ini - - - - https://raw.githubusercontent.com/joomla-extensions/weblinks/master/manifest.xml - - diff --git a/administrator/modules/mod_login/mod_login.php b/administrator/modules/mod_login/mod_login.php index a972336b4ff38..74dd4632f29e4 100644 --- a/administrator/modules/mod_login/mod_login.php +++ b/administrator/modules/mod_login/mod_login.php @@ -14,6 +14,7 @@ $langs = ModLoginHelper::getLanguageList(); $twofactormethods = JAuthenticationHelper::getTwoFactorMethods(); +$extraFields = JAuthenticationHelper::getUserLoginFormFields(); $return = ModLoginHelper::getReturnUri(); require JModuleHelper::getLayoutPath('mod_login', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_login/tmpl/default.php b/administrator/modules/mod_login/tmpl/default.php index 7384869c9d5a4..8cd0a8ea410cd 100644 --- a/administrator/modules/mod_login/tmpl/default.php +++ b/administrator/modules/mod_login/tmpl/default.php @@ -70,6 +70,32 @@ + getType() != 'field') continue; + ?> +
+
+ get('usetext')) : ?> +
+ + + + + + getInput(); ?> +
+ + + getInput(); ?> + +
+
+ +
@@ -91,6 +117,20 @@ + getType() != 'link') continue; + ?> + + getIcon()): ?> + + + getLabel(); ?> + + +
diff --git a/administrator/templates/hathor/html/mod_login/default.php b/administrator/templates/hathor/html/mod_login/default.php index 993a3a976ace9..0585c3aecbaf4 100644 --- a/administrator/templates/hathor/html/mod_login/default.php +++ b/administrator/templates/hathor/html/mod_login/default.php @@ -29,6 +29,20 @@ + getType() != 'field') continue; + ?> +
+
+ + getInput(); ?> +
+
+ + @@ -43,6 +57,24 @@ + getType() != 'button') continue; + ?> +
+ +
+ +
diff --git a/components/com_users/models/registration.php b/components/com_users/models/registration.php index 64c3d5d897b2d..ad2a1b4ce291e 100644 --- a/components/com_users/models/registration.php +++ b/components/com_users/models/registration.php @@ -372,18 +372,21 @@ protected function populateState() /** * Method to save the form data. * - * @param array $temp The form data. + * @param array $temp The form data. + * @param bool $verified Is this user account / email address pre-verified (e.g. through a social network integration)? * * @return mixed The user id on success, false on failure. * * @since 1.6 */ - public function register($temp) + public function register($temp, $verified = false) { $params = JComponentHelper::getParams('com_users'); // Initialise the table with JUser. $user = new JUser; + + // Get the default data from the site's setup $data = (array) $this->getData(); // Merge in the registration data. @@ -398,6 +401,12 @@ public function register($temp) $useractivation = $params->get('useractivation'); $sendpassword = $params->get('sendpassword', 1); + // If the email / user account is pre-verified there's no need to send an activation email. + if ($verified) + { + $useractivation = 0; + } + // Check if the user needs to activate their account. if (($useractivation == 1) || ($useractivation == 2)) { diff --git a/components/com_users/views/login/tmpl/default_login.php b/components/com_users/views/login/tmpl/default_login.php index 7f6c7ccda1bd8..8f4752aefd3c0 100644 --- a/components/com_users/views/login/tmpl/default_login.php +++ b/components/com_users/views/login/tmpl/default_login.php @@ -11,6 +11,9 @@ JHtml::_('behavior.keepalive'); JHtml::_('behavior.formvalidator'); + +/** @var UsersViewLogin $this */ + ?>
params->get('show_page_heading')) : ?> @@ -64,6 +67,21 @@
+ extraFields)): + foreach ($this->extraFields as $extraField): + if ($extraField->getType() != 'field') continue; + ?> +
+
+ +
+
+ getInput(); ?> +
+
+ + +
@@ -76,6 +94,19 @@ + extraFields)): + $extraFieldCounter = 0; + foreach ($this->extraFields as $extraField): + if ($extraField->getType() != 'button') continue; + ?> + + getIcon()): ?> + + + getLabel(); ?> + + +
@@ -106,5 +137,17 @@ + extraFields)): + $extraFieldCounter = 0; + foreach ($this->extraFields as $extraField): + if ($extraField->getType() != 'link') continue; + ?> +
  • + + getLabel(); ?> + +
  • + + diff --git a/components/com_users/views/login/view.html.php b/components/com_users/views/login/view.html.php index 753e89e9ab565..4c4b80d00820e 100644 --- a/components/com_users/views/login/view.html.php +++ b/components/com_users/views/login/view.html.php @@ -16,12 +16,31 @@ */ class UsersViewLogin extends JViewLegacy { + /** + * @var JForm + */ protected $form; + /** + * Additional login form fields + * + * @var JAuthenticationFieldInterface[] + * + * @since 3.7 + */ + protected $extraFields; + + /** + * Users component parameters + * + * @var \Joomla\Registry\Registry + */ protected $params; protected $state; + protected $tfa; + protected $user; /** @@ -57,9 +76,20 @@ public function display($tpl = null) $this->setLayout($active->query['layout']); } - $tfa = JAuthenticationHelper::getTwoFactorMethods(); + $tfa = JAuthenticationHelper::getTwoFactorMethods(); $this->tfa = is_array($tfa) && count($tfa) > 1; + if ($this->params->get('login_redirect_url')) + { + $returnUrl = $this->params->get('login_redirect_url', $this->form->getValue('return')); + } + else + { + $returnUrl = $this->params->get('login_redirect_menuitem', $this->form->getValue('return')); + } + + $this->extraFields = JAuthenticationHelper::getUserLoginFormFields($returnUrl); + // Escape strings for HTML output $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx'), ENT_COMPAT, 'UTF-8'); diff --git a/installation/sql/mysql/joomla.sql b/installation/sql/mysql/joomla.sql index 89c0e7bcf212c..7a77824e333a7 100644 --- a/installation/sql/mysql/joomla.sql +++ b/installation/sql/mysql/joomla.sql @@ -614,6 +614,7 @@ INSERT INTO `#__extensions` (`extension_id`, `name`, `type`, `element`, `folder` (455, 'plg_installer_packageinstaller', 'plugin', 'packageinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '0000-00-00 00:00:00', 1, 0), (456, 'plg_installer_folderinstaller', 'plugin', 'folderinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '0000-00-00 00:00:00', 2, 0), (457, 'plg_installer_urlinstaller', 'plugin', 'urlinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '0000-00-00 00:00:00', 3, 0), +(458, 'plg_authentication_facebook', 'plugin', 'facebook', 'authentication', 0, 0, 1, 1, '', '', '', '', 0, '0000-00-00 00:00:00', 3, 0), (503, 'beez3', 'template', 'beez3', '', 0, 1, 1, 0, '', '{"wrapperSmall":"53","wrapperLarge":"72","sitetitle":"","sitedescription":"","navposition":"center","templatecolor":"nature"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), (504, 'hathor', 'template', 'hathor', '', 1, 1, 1, 0, '', '{"showSiteName":"0","colourChoice":"0","boldText":"0"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), (506, 'protostar', 'template', 'protostar', '', 0, 1, 1, 0, '', '{"templateColor":"","logoFile":"","googleFont":"1","googleFontName":"Open+Sans","fluidContainer":"0"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), diff --git a/installation/sql/postgresql/joomla.sql b/installation/sql/postgresql/joomla.sql index f946df699fd41..9a02807f7689e 100644 --- a/installation/sql/postgresql/joomla.sql +++ b/installation/sql/postgresql/joomla.sql @@ -613,7 +613,8 @@ INSERT INTO "#__extensions" ("extension_id", "name", "type", "element", "folder" (454, 'plg_system_stats', 'plugin', 'stats', 'system', 0, 1, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0), (455, 'plg_installer_packageinstaller', 'plugin', 'packageinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '1970-01-01 00:00:00', 1, 0), (456, 'plg_installer_folderinstaller', 'plugin', 'folderinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '1970-01-01 00:00:00', 2, 0), -(457, 'plg_installer_urlinstaller', 'plugin', 'urlinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '1970-01-01 00:00:00', 3, 0); +(457, 'plg_installer_urlinstaller', 'plugin', 'urlinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '1970-01-01 00:00:00', 3, 0), +(458, 'plg_authentication_facebook', 'plugin', 'facebook', 'authentication', 0, 1, 1, 1, '', '', '', '', 0, '1970-01-01 00:00:00', 3, 0); -- Templates INSERT INTO "#__extensions" ("extension_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES diff --git a/installation/sql/sqlazure/joomla.sql b/installation/sql/sqlazure/joomla.sql index fc4e980e376c5..1021aa993d7fc 100644 --- a/installation/sql/sqlazure/joomla.sql +++ b/installation/sql/sqlazure/joomla.sql @@ -1010,7 +1010,9 @@ SELECT 455, 'plg_installer_packageinstaller', 'plugin', 'packageinstaller', 'ins UNION ALL SELECT 456, 'plg_installer_folderinstaller', 'plugin', 'folderinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '1900-01-01 00:00:00', 2, 0 UNION ALL -SELECT 457, 'plg_installer_urlinstaller', 'plugin', 'urlinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '1900-01-01 00:00:00', 3, 0; +SELECT 457, 'plg_installer_urlinstaller', 'plugin', 'urlinstaller', 'installer', 0, 1, 1, 1, '', '', '', '', 0, '1900-01-01 00:00:00', 3, 0 +UNION ALL +SELECT 458, 'plg_authentication_facebook', 'plugin', 'facebook', 'authentication', 0, 1, 1, 1, '', '', '', '', 0, '1900-01-01 00:00:00', 3, 0; -- Templates INSERT INTO [#__extensions] ([extension_id], [name], [type], [element], [folder], [client_id], [enabled], [access], [protected], [manifest_cache], [params], [custom_data], [system_data], [checked_out], [checked_out_time], [ordering], [state]) diff --git a/libraries/cms/authentication/field/interface.php b/libraries/cms/authentication/field/interface.php new file mode 100644 index 0000000000000..a2407465ec78a --- /dev/null +++ b/libraries/cms/authentication/field/interface.php @@ -0,0 +1,66 @@ +trigger('onUserTwofactorIdentify', array()); @@ -53,4 +60,129 @@ public static function getTwoFactorMethods() return $options; } + + /** + * Get the additional login form field definitions. + * + * @param string $loginUrl The URL or menu item ID to return to after successfully logging in + * @param string $failureUrl The URL or menu item ID to return to after failing to log in + * + * @return array Additional login form field definitions + * + * @since 3.7 + */ + public static function getUserLoginFormFields($loginUrl = null, $failureUrl = null) + { + $loginUrl = self::cleanUpReturnUrl($loginUrl); + $failureUrl = self::cleanUpReturnUrl($failureUrl); + + /** + * All three types of plugins can define custom login fields. + * + * While custom login fields are primarily used for social login, opening up the implementation to Two Factor + * Authentication and User plugins lets us implement richer login interactions. + */ + JPluginHelper::importPlugin('twofactorauth'); + JPluginHelper::importPlugin('authentication'); + JPluginHelper::importPlugin('user'); + + $fieldDefinitions = JEventDispatcher::getInstance()->trigger('onUserLoginFormFields', array($loginUrl, $failureUrl)); + + $fields = array(); + + if (!empty($fieldDefinitions)) + { + foreach ($fieldDefinitions as $fieldDefinition) + { + if (!is_array($fieldDefinition)) + { + continue; + } + + foreach ($fieldDefinition as $field) + { + if (!is_object($field) || !($field instanceof JAuthenticationFieldInterface)) + { + continue; + } + + $fields[] = $field; + } + } + } + + return $fields; + } + + /** + * Cleans up a return URL. If it's a URL it makes sure it's an internal URL. If it's a menu item ID it creates the + * absolute URL pointing to it. If the URL is empty or non-internal the current URL is used instead. + * + * @param string $return The return URL to clean up + * + * @return string The cleaned up URL + */ + protected static function cleanUpReturnUrl($return) + { + // Get the default URL (current page) + $defaultUrl = JUri::getInstance()->toString(); + + // Is the return URL base64-encoded by any chance? + $returnDecoded = @base64_decode($return); + + if ($returnDecoded !== false) + { + $return = $returnDecoded; + } + + // Do we have a URL (as opposed to a menu item)? + if (!is_numeric($return)) + { + // Don't redirect to an external URL. + if (!JUri::isInternal($return)) + { + return $defaultUrl; + } + + if (strpos($return, 'index.php?') !== false) + { + $return = JRoute::_($return); + } + + return empty($return) ? $defaultUrl : $return; + } + + // We have a menu item. We must translate it to a URL. For this, we need the language ID suffix. + $lang = ''; + + if (JLanguageMultilang::isEnabled()) + { + + $db = JFactory::getDbo(); + $query = $db->getQuery(true) + ->select('language') + ->from($db->quoteName('#__menu')) + ->where('client_id = 0') + ->where('id =' . $return); + + $db->setQuery($query); + + try + { + $language = $db->loadResult(); + } + catch (RuntimeException $e) + { + return $defaultUrl; + } + + if ($language !== '*') + { + $lang = '&lang=' . $language; + } + } + + // Construct and reeturn the URL + return JRoute::_('index.php?Itemid=' . $return . $lang); + } } diff --git a/libraries/cms/plugin/authentication/social.php b/libraries/cms/plugin/authentication/social.php new file mode 100644 index 0000000000000..c4564b672bf76 --- /dev/null +++ b/libraries/cms/plugin/authentication/social.php @@ -0,0 +1,271 @@ +deriveUsername($name); + $userId = JUserHelper::getUserId($username); + + // Does an account with the same username already exist on our site? + if ($userId != 0) + { + throw new UnexpectedValueException(); + } + + $randomPassword = JUserHelper::genRandomPassword(32); + $data = array( + 'name' => $name, + 'username' => $this->deriveUsername($username), + 'password1' => $randomPassword, + 'password2' => $randomPassword, + 'email1' => JStringPunycode::emailToPunycode($email), + 'email2' => JStringPunycode::emailToPunycode($email), + ); + + // Load com_users language, because the model doesn't do it automatically + $jLanguage = JFactory::getLanguage(); + $jLanguage->load('com_users', JPATH_BASE, 'en-GB', true); + $jLanguage->load('com_users', JPATH_BASE, null, false); + + // Load the Registration model of com_users and register the new user. + JModelLegacy::addIncludePath(JPATH_SITE . '/components/com_users/models', 'UsersModel'); + + /** @var UsersModelRegistration $model */ + $model = JModelLegacy::getInstance('Registration', 'UsersModel', array('ignore_request' => true)); + + /** + * Why do we pass the $verified flag? TL;DR: to streamline the user experience without discounting security. + * + * We do not need to send an account verification email to accounts verified by a third party. For example, in + * the case of Facebook login we may have a verified Facebook account. These accounts have already had their + * email or phone number verified by Facebook. Therefore verified Facebook accounts get immediate access to our + * site, as the users would expect. Unverified accounts have to go through the whole email verification process. + */ + $userId = $model->register($data, $verified); + + // Internal error setting up account? + if ($userId === false) + { + throw new RuntimeException($model->getError()); + } + + // Update the time offset + if (is_numeric($userId) && !is_null($offset)) + { + $user = JFactory::getUser($userId); + $user->setParam('offset', $offset); + $user->save(true); + } + + // Return the user ID + return $userId; + } + + /** + * Logs in a user. We use this method to override authentication and Two Factor Authentication plugins (since we are + * essentially implementing a single sign on where Facebook acts as our SSO authorization server). + * + * @param int $userId Joomla! user ID + * + * @return bool True on success + * + * @throws Exception + * + * @since 3.7 + */ + protected function loginUser($userId) + { + JLoader::import('joomla.user.authentication'); + + /** + * We need this line to load the JAuthentication class file. That file ALSO defines the JAuthenticationResponse + * class. That's bad design which dates back to Joomla! 1.5 (possibly 1.0?) and which we can't change for b/c + * reasons. Do NOT delete this line! + */ + class_exists('JAuthentication'); + + $user = JUser::getInstance($userId); + $response = $this->getAuthenticationResponseObject($user); + + // If the user doesn't exist + if (empty($user->id)) + { + $response->status = JAuthentication::STATUS_UNKNOWN; + $response->error_message = JText::_('JGLOBAL_AUTH_NO_USER'); + $this->processLoginFailure($response); + + return false; + } + + // If the user is blocked + if ($user->block == 1) + { + $response->status = JAuthentication::STATUS_FAILURE; + $response->error_message = JText::_('JERROR_NOLOGIN_BLOCKED'); + $this->processLoginFailure($response); + + return false; + } + + // If the user is not activated yet + if ($user->activation) + { + $response->status = JAuthentication::STATUS_FAILURE; + $response->error_message = JText::_('JERROR_NOLOGIN_BLOCKED'); + $this->processLoginFailure($response); + + return false; + } + + JPluginHelper::importPlugin('user'); + $options = array('remember' => true); + JEventDispatcher::getInstance()->trigger('onLoginUser', array((array) $response, $options)); + + $session = JFactory::getSession(); + $session->set('user', $user); + + return true; + } + + /** + * Returns a generic JAuthenticationResponse object + * + * @return JAuthenticationResponse + * + * @since 3.7 + */ + protected function getAuthenticationResponseObject(JUser $user = null) + { + JLoader::import('joomla.user.authentication'); + + /** + * We need this line to load the JAuthentication class file. That file ALSO defines the JAuthenticationResponse + * class. That's bad design which dates back to Joomla! 1.5 (possibly 1.0?) and which we can't change for b/c + * reasons. Do NOT delete this line! + */ + class_exists('JAuthentication'); + + $response = new JAuthenticationResponse(); + $response->status = JAuthentication::STATUS_UNKNOWN; + $response->type = $this->_name; + $response->error_message = ''; + $response->username = ''; + $response->email = ''; + $response->fullname = ''; + $response->language = null; + + if (is_object($user)) + { + $response->username = $user->username; + $response->email = $user->email; + $response->fullname = $user->name; + $response->language = $user->getParam('language'); + + if (JFactory::getApplication()->isAdmin()) + { + $response->language = $user->getParam('admin_language'); + } + } + + return $response; + } + + /** + * Logs a login failure by triggering the onUserLoginFailure event in the application. It will enqueue any error + * messages for display. + * + * @param JAuthenticationResponse $response The authentication response object + * + * @since 3.7 + */ + protected function processLoginFailure(JAuthenticationResponse $response) + { + JPluginHelper::importPlugin('user'); + + $app = JFactory::getApplication(); + $app->triggerEvent('onUserLoginFailure', array((array) $response)); + + if (!empty($response->error_message)) + { + $app->enqueueMessage($response->error_message, 'error'); + } + } +} \ No newline at end of file diff --git a/libraries/joomla/user/helper.php b/libraries/joomla/user/helper.php index c03e924c8d101..727420d6040c2 100644 --- a/libraries/joomla/user/helper.php +++ b/libraries/joomla/user/helper.php @@ -295,6 +295,28 @@ public static function getUserId($username) return $db->loadResult(); } + /** + * Returns userid if a user exists + * + * @param string $email The email to search on. + * + * @return integer The user id or 0 if not found. + * + * @since 11.1 + */ + public static function getUserIdByEmail($email) + { + // Initialise some variables + $db = JFactory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('email') . ' = ' . $db->quote($email)); + $db->setQuery($query, 0, 1); + + return $db->loadResult(); + } + /** * Hashes a password using the current encryption. * diff --git a/media/plg_authentication_facebook/images/fb.png b/media/plg_authentication_facebook/images/fb.png new file mode 100755 index 0000000000000..ad93b5936ff35 Binary files /dev/null and b/media/plg_authentication_facebook/images/fb.png differ diff --git a/modules/mod_login/mod_login.php b/modules/mod_login/mod_login.php index 894680d3292ed..733a6bc1d7a2c 100644 --- a/modules/mod_login/mod_login.php +++ b/modules/mod_login/mod_login.php @@ -17,6 +17,7 @@ $type = ModLoginHelper::getType(); $return = ModLoginHelper::getReturnUrl($params, $type); $twofactormethods = JAuthenticationHelper::getTwoFactorMethods(); +$extraFields = JAuthenticationHelper::getUserLoginFormFields($return); $user = JFactory::getUser(); $layout = $params->get('layout', 'default'); diff --git a/modules/mod_login/tmpl/default.php b/modules/mod_login/tmpl/default.php index 225fd9b2b1762..1e9cb4d4d254e 100644 --- a/modules/mod_login/tmpl/default.php +++ b/modules/mod_login/tmpl/default.php @@ -83,6 +83,34 @@ + + getType() != 'field') continue; + ?> +
    +
    + get('usetext')) : ?> +
    + + + + + + getInput(); ?> +
    + + + getInput(); ?> + +
    +
    + + +
    @@ -91,6 +119,20 @@
    + getType() != 'button') continue; + ?> + + getIcon()): ?> + + + getLabel(); ?> + + +
    "> + getType() != 'link') continue; + ?> +
  • + + getLabel(); ?> + +
  • + + diff --git a/plugins/authentication/facebook/facebook.php b/plugins/authentication/facebook/facebook.php new file mode 100644 index 0000000000000..39f808f2c3b22 --- /dev/null +++ b/plugins/authentication/facebook/facebook.php @@ -0,0 +1,388 @@ +isAdmin()) + { + return array(); + } + + // Set the return URLs into the session + $session = JFactory::getSession(); + $session->set('loginUrl', $loginUrl, 'plg_authenticate_facebook'); + $session->set('failureUrl', $failureUrl, 'plg_authenticate_facebook'); + + // Load plugin language + $this->loadLanguage('plg_authentication_facebook'); + + // Initialize the fields + $fields = array(); + + // Try to get a Facebook custom field + try + { + $appId = $this->params->get('appid', ''); + $appSecret = $this->params->get('appsecret', ''); + + if (empty($appId) || empty($appSecret)) + { + throw new InvalidArgumentException('Cannot have an empty Facebook application ID or Secret'); + } + + $fields[] = new JAuthenticationFieldFacebook($this->getFacebookOauth()); + } + catch (InvalidArgumentException $e) + { + $fields = array(); + } + + return $fields; + } + + /** + * Processes the authentication callback from Facebook. + * + * @return void + */ + public function onAjaxFacebook() + { + // Load plugin language + $this->loadLanguage('plg_authentication_facebook'); + + // Get the return URLs from the session + $session = JFactory::getSession(); + $loginUrl = $session->get('loginUrl', null, 'plg_authenticate_facebook'); + $failureUrl = $session->get('failureUrl', null, 'plg_authenticate_facebook'); + + // Remove the return URLs from the session + $session->set('loginUrl', null, 'plg_authenticate_facebook'); + $session->set('failureUrl', null, 'plg_authenticate_facebook'); + + // Try to exchange the code with a token + $facebookOauth = $this->getFacebookOauth(); + $app = JFactory::getApplication(); + + try + { + $token = $facebookOauth->authenticate(); + + if ($token === false) + { + throw new RuntimeException(JText::_('PLG_AUTHENTICATION_FACEBOOK_ERROR_NOT_LOGGED_IN_FB')); + } + + // Get information about the user from Big Brother... er... Facebook. + $options = new Registry(); + $options->set('api.url', 'https://graph.facebook.com/v2.7/'); + $fbUserApi = new JFacebookUser($options, null, $facebookOauth); + $fbUserFields = $fbUserApi->getUser('me?fields=id,name,email,verified,timezone'); + + $fullName = $fbUserFields->name; + $fbUserId = $fbUserFields->id; + $fbUserEmail = $fbUserFields->email; + $fbUserVerified = $fbUserFields->verified; + $fbUserGMTOffset = $fbUserFields->timezone; + } + catch (Exception $e) + { + // Log failed login + $response = $this->getAuthenticationResponseObject(); + $response->status = JAuthentication::STATUS_UNKNOWN; + $response->error_message = JText::sprintf('JGLOBAL_AUTH_FAILED', $e->getMessage()); + $this->processLoginFailure($response); + + $app->redirect($failureUrl); + + return; + } + + // Look for a local user account with the Facebook user ID + $userId = $this->getUserIdByFacebookId($fbUserId); + + /** + * Does a user exist with the same email as the Facebook email? + * + * We only do that for verified Facebook users, i.e. people who have already verified that they have control of + * their stated email address and / or phone with Facebook. This is a security measure! It prevents someone from + * registering a Facebook account under your email address (without verifying that email address) and use it to + * login into the Joomla site impersonating you. + */ + if ($fbUserVerified && ($userId == 0)) + { + $userId = JUserHelper::getUserIdByEmail($fbUserEmail); + } + + if (empty($userId)) + { + $usersConfig = JComponentHelper::getParams('com_users'); + $allowUserRegistration = $usersConfig->get('allowUserRegistration'); + + // User not found and user registration is disabled + if ($allowUserRegistration == 0) + { + // Log failed login + $response = $this->getAuthenticationResponseObject(); + $response->status = JAuthentication::STATUS_UNKNOWN; + $response->error_message = JText::sprintf('JGLOBAL_AUTH_FAILED', JText::_('PLG_AUTHENTICATION_FACEBOOK_ERROR_LOCAL_NOT_FOUND')); + $this->processLoginFailure($response); + + $app->redirect($failureUrl); + + return; + } + + try + { + $userId = $this->createUser($fbUserEmail, $fullName, $fbUserVerified, $fbUserGMTOffset); + } + catch (UnexpectedValueException $e) + { + // Log failure to create user (username already exists) + $response = $this->getAuthenticationResponseObject(); + $response->status = JAuthentication::STATUS_UNKNOWN; + $response->error_message = JText::sprintf('PLG_AUTHENTICATION_FACEBOOK_ERROR_CANNOT_CREATE', JText::_('PLG_AUTHENTICATION_FACEBOOK_ERROR_LOCAL_USERNAME_CONFLICT')); + $this->processLoginFailure($response); + + $app->redirect($failureUrl); + + return; + } + catch (RuntimeException $e) + { + // Log failure to create user (other internal error, check the model error message returned in the exception) + $response = $this->getAuthenticationResponseObject(); + $response->status = JAuthentication::STATUS_UNKNOWN; + $response->error_message = JText::sprintf('PLG_AUTHENTICATION_FACEBOOK_ERROR_CANNOT_CREATE', $e->getMessage()); + $this->processLoginFailure($response); + + $app->redirect($failureUrl); + + return; + } + + // Does the account need user or administrator verification? + if (in_array($userId, array('useractivate', 'adminactivate'))) + { + // Do NOT go through processLoginFailure. This is NOT a login failure. + $message = JText::_('PLG_AUTHENTICATION_FACEBOOK_NOTICE_' . $userId); + + $app->enqueueMessage($message, 'info'); + $app->redirect($failureUrl); + + return; + } + } + + // Attach the Facebook user ID and token to the user's profile + try + { + self::linkToFacebook($userId, $fbUserId, $token); + } + catch (Exception $e) + { + // Ignore database exceptions at this point + } + + // Log in the user + if ($this->loginUser($userId)) + { + $app->redirect($loginUrl); + + return; + } + + $app->redirect($failureUrl); + } + + /** + * Returns a JFacebookOAuth object + * + * @return JFacebookOAuth + * + * @since 3.7 + */ + protected function getFacebookOauth() + { + if (is_null($this->facebook)) + { + $appId = $this->params->get('appid', ''); + $appSecret = $this->params->get('appsecret', ''); + + $options = new Registry(array( + 'clientid' => $appId, + 'clientsecret' => $appSecret, + 'redirecturi' => JUri::base() . 'index.php?option=com_ajax&group=authentication&plugin=facebook&format=raw' + )); + + $this->facebook = new JFacebookOAuth($options); + $this->facebook->setScope('public_profile,email'); + } + + return $this->facebook; + } + + /** + * Links the user account to the Facebook account through User Profile fields + * + * @param int $userId The Joomla! user ID + * @param int $fbUserId The Facebook user ID + * @param string $token The Facebook OAuth token + * + * @return void + * + * @since 3.7 + */ + protected static function linkToFacebook($userId, $fbUserId, $token) + { + // Load the profile data from the database. + $db = JFactory::getDbo(); + $query = $db->getQuery(true) + ->select(array( + $db->qn('profile_key'), + $db->qn('profile_value'), + ))->from($db->qn('#__user_profiles')) + ->where($db->qn('user_id') . ' = ' . $db->q((int) $userId)) + ->where($db->qn('profile_key') . ' LIKE ' . $db->q('facebook.%')) + ->order($db->qn('ordering')); + $fields = $db->setQuery($query)->loadAssocList('profile_key', 'profile_value'); + + if (!isset($fields['facebook.userid'])) + { + $newField = (object) array( + 'user_id' => $userId, + 'profile_key' => 'facebook.userid', + 'profile_value' => $fbUserId, + 'ordering' => 0 + ); + + $db->insertObject('#__user_profiles', $newField); + } + elseif ($fields['facebook.userid'] != $fbUserId) + { + $query = $db->getQuery(true) + ->update($db->qn('#__user_profiles')) + ->set($db->qn('profile_value') . ' = ' . $db->q($fbUserId)) + ->where($db->qn('user_id') . ' = ' . $db->q((int) $userId)) + ->where($db->qn('profile_key') . ' = ' . $db->q('facebook.userid')); + $db->setQuery($query)->execute(); + } + + $token = json_encode($token); + + if (!isset($fields['facebook.token'])) + { + $newField = (object) array( + 'user_id' => $userId, + 'profile_key' => 'facebook.token', + 'profile_value' => $token, + 'ordering' => 0 + ); + + $db->insertObject('#__user_profiles', $newField); + } + elseif ($fields['facebook.token'] != $token) + { + $query = $db->getQuery(true) + ->update($db->qn('#__user_profiles')) + ->set($db->qn('profile_value') . ' = ' . $db->q($token)) + ->where($db->qn('user_id') . ' = ' . $db->q((int) $userId)) + ->where($db->qn('profile_key') . ' = ' . $db->q('facebook.token')); + $db->setQuery($query)->execute(); + } + } + + /** + * Gets the Joomla! user ID that corresponds to a Facebook user ID. Of course that implies that the user has logged + * in to the Joomla! site through Facebook in the past or he has otherwise linked his user account to Facebook. + * + * @param string $fbUserId The Facebook User ID. + * + * @return int The corresponding user ID or 0 if no user is found + * + * @since 3.7 + */ + protected function getUserIdByFacebookId($fbUserId) + { + $db = JFactory::getDbo(); + $query = $db->getQuery(true) + ->select(array( + $db->qn('user_id'), + ))->from($db->qn('#__user_profiles')) + ->where($db->qn('profile_key') . ' = ' . $db->q('facebook.userid')) + ->where($db->qn('profile_value') . ' = ' . $db->q($fbUserId)); + + try + { + $id = $db->setQuery($query, 0, 1)->loadResult(); + + // Not found? + if (empty($id)) + { + return 0; + } + + /** + * If you delete a user its profile fields are left behind and confuse our code. Therefore we have to check + * if the user *really* exists. However we can't just go through JFactory::getUser() because if the user + * does not exist we'll end up with an ugly Warning on our page with a text similar to "JUser: :_load: + * Unable to load user with ID: 1234". This cannot be disabled so we have to be, um, a bit creative :/ + */ + $db = JFactory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)')->from($db->qn('#__users')) + ->where($db->qn('id') . ' = ' . $db->q($id)); + $userExists = $db->setQuery($query)->loadResult(); + + return ($userExists == 0) ? 0 : $id; + } + catch (Exception $e) + { + return 0; + } + } +} diff --git a/plugins/authentication/facebook/facebook.xml b/plugins/authentication/facebook/facebook.xml new file mode 100644 index 0000000000000..fcb20881defaf --- /dev/null +++ b/plugins/authentication/facebook/facebook.xml @@ -0,0 +1,52 @@ + + + plg_authentication_facebook + Joomla! Project + August 2016 + Copyright (C) 2005 - 2016 Open Source Matters. All rights reserved. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 3.7.0 + PLG_AUTHENTICATION_FACEBOOK_XML_DESCRIPTION + + facebook.php + + + en-GB.plg_authentication_facebook.ini + en-GB.plg_authentication_facebook.sys.ini + + + +
    + + + + + + + + + + +
    +
    +
    +
    diff --git a/plugins/authentication/facebook/field/facebook.php b/plugins/authentication/facebook/field/facebook.php new file mode 100644 index 0000000000000..97c0c75e0d629 --- /dev/null +++ b/plugins/authentication/facebook/field/facebook.php @@ -0,0 +1,60 @@ +url = $facebook->createUrl(); + } + + public function getType() + { + return 'button'; + } + + public function getIcon() + { + return ''; + } + + public function getLabel() + { + $text = JText::_('PLG_AUTHENTICATION_FACEBOOK_BTN_LABEL'); + + return JHtml::_('image', 'plg_authentication_facebook/fb.png', $text, array('title' => $text, 'width' => 16), true) . + ' ' . $text; + } + + public function getInput() + { + return $this->url; + } +} diff --git a/templates/beez3/html/mod_login/default.php b/templates/beez3/html/mod_login/default.php index 467c3d71a6e8e..380eb67de7eca 100644 --- a/templates/beez3/html/mod_login/default.php +++ b/templates/beez3/html/mod_login/default.php @@ -42,6 +42,27 @@
    +getType() != 'field') continue; + ?> +
    +
    + get('usetext')) : ?> +
    + + getInput(); ?> +
    + + + getInput(); ?> + +
    +
    + +

    @@ -49,6 +70,20 @@

    +getType() != 'button') continue; + ?> + + + @@ -68,6 +103,19 @@ + getType() != 'link') continue; + ?> +
  • + + getLabel(); ?> + +
  • + + get('posttext')) : ?>