From 2140a3d67dc0853cdc3cb9ed634fd7ba36a8da03 Mon Sep 17 00:00:00 2001 From: Martina Scholz Date: Sun, 27 Apr 2025 16:12:29 +0000 Subject: [PATCH] Inital commit --- .../com_config/forms/application.xml | 9 + .../src/Controller/CallbackController.php | 84 +++++++ administrator/language/en-GB/com_config.ini | 2 + administrator/language/en-GB/lib_joomla.ini | 8 + .../src/Form/Field/OAuth2ClientField.php | 216 ++++++++++++++++++ .../src/Mail/MailerOAuthTokenProvider.php | 149 ++++++++++++ 6 files changed, 468 insertions(+) create mode 100644 administrator/components/com_config/src/Controller/CallbackController.php create mode 100644 libraries/src/Form/Field/OAuth2ClientField.php create mode 100644 libraries/src/Mail/MailerOAuthTokenProvider.php diff --git a/administrator/components/com_config/forms/application.xml b/administrator/components/com_config/forms/application.xml index 8f48019424b54..58658564fce83 100644 --- a/administrator/components/com_config/forms/application.xml +++ b/administrator/components/com_config/forms/application.xml @@ -537,6 +537,7 @@ + + +
+ * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Config\Administrator\Controller; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Event\MultiFactor\Callback; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\BaseController; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Input\Input; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * OAuth2 AJAX callback controller for mailer + * + * @since __DEPLOY_VERSION__ + */ +class CallbackController extends BaseController +{ + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param ?MVCFactoryInterface $factory MVC Factory for the com_users component + * @param ?CMSApplication $app CMS application object + * @param ?Input $input Joomla CMS input object + * + * @since __DEPLOY_VERSION__ + */ + public function __construct(array $config = [], ?MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerDefaultTask('mailer'); + } + + /** + * Handles an OAuth Callback request for OAuth2 + * + * @param bool $cachable Can this view be cached + * @param array|bool $urlparams An array of safe url parameters and their variable types. + * @see \Joomla\CMS\Filter\InputFilter::clean() for valid values. + * + * @return void + * @since __DEPLOY_VERSION__ + */ + protected function callback($cachable = false, $urlparams = false): void + { + /** + * If we are still here and no method handled the request successfully. Show an error. + */ + throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** + * Handles an OAuth Callback request for phpmailer + * + * URLs containing [sitename]/administrator/index.php?option=com_config&task=callback.mailer + * + * @param bool $cachable Can this view be cached + * @param array|bool $urlparams An array of safe url parameters and their variable types. + * @see \Joomla\CMS\Filter\InputFilter::clean() for valid values. + * + * @return void + * @since __DEPLOY_VERSION__ + */ + public function mailer($cachable = false, $urlparams = false): void + { + // Redirect + $this->redirect(); + } +} diff --git a/administrator/language/en-GB/com_config.ini b/administrator/language/en-GB/com_config.ini index 027846bfb3c83..3f14a54f20b37 100644 --- a/administrator/language/en-GB/com_config.ini +++ b/administrator/language/en-GB/com_config.ini @@ -121,6 +121,7 @@ COM_CONFIG_FIELD_LOG_PRIORITIES_WARNING="Warning" COM_CONFIG_FIELD_MAIL_FROM_EMAIL_LABEL="From Email" COM_CONFIG_FIELD_MAIL_FROM_NAME_LABEL="From Name" COM_CONFIG_FIELD_MAIL_MAILER_LABEL="Mailer" +COM_CONFIG_FIELD_MAIL_SMTP_OAUTH2CLIENT_LABEL="SMTP XOAuth" COM_CONFIG_FIELD_MAIL_MAILONLINE_LABEL="Send Mail" COM_CONFIG_FIELD_MAIL_MASSMAILOFF_DESC="Joomla offers a Mass Mail feature which allows a user with administrator access to send an email to all users of the site. On sites with more than a few dozen users this can be problematic or time out." COM_CONFIG_FIELD_MAIL_MASSMAILOFF_LABEL="Disable Mass Mail" @@ -204,6 +205,7 @@ COM_CONFIG_FIELD_VALUE_SENDMAIL="Sendmail" COM_CONFIG_FIELD_VALUE_SIMPLE="Simple" COM_CONFIG_FIELD_VALUE_SITE_EMAIL="Site Email" COM_CONFIG_FIELD_VALUE_SMTP="SMTP" +COM_CONFIG_FIELD_VALUE_SMTPOAUTH="SMTP OAuth2" COM_CONFIG_FIELD_VALUE_SSL="SSL/TLS" COM_CONFIG_FIELD_VALUE_SYSTEM_DEFAULT="System Default" COM_CONFIG_FIELD_VALUE_TLS="STARTTLS" diff --git a/administrator/language/en-GB/lib_joomla.ini b/administrator/language/en-GB/lib_joomla.ini index f9ea658e0291d..afc1017ca27fa 100644 --- a/administrator/language/en-GB/lib_joomla.ini +++ b/administrator/language/en-GB/lib_joomla.ini @@ -318,6 +318,14 @@ JLIB_FORM_FIELD_PARAM_MEDIA_PREVIEW_DESC="Shows or hides the preview of the sele JLIB_FORM_FIELD_PARAM_MEDIA_PREVIEW_INLINE="Inline" JLIB_FORM_FIELD_PARAM_MEDIA_PREVIEW_LABEL="Preview" JLIB_FORM_FIELD_PARAM_MEDIA_PREVIEW_TOOLTIP="Tooltip" +JLIB_FORM_FIELD_PARAM_OAUTH2CLIENT_LABEL="OAuth2 Client" +JLIB_FORM_FIELD_PARAM_OAUTH_PROVIDER_LABEL="Provider" ; @todo ??? +JLIB_FORM_FIELD_PARAM_OAUTH_TOKEN_URL_LABEL="Token URL" +JLIB_FORM_FIELD_PARAM_OAUTH_AUTH_URL_LABEL="Authorization URL" +JLIB_FORM_FIELD_PARAM_OAUTH_URL_LABEL="OAuth URL" +JLIB_FORM_FIELD_PARAM_OAUTH_CLIENT_ID_LABEL="Client ID" +JLIB_FORM_FIELD_PARAM_OAUTH_CLIENT_SECRET_LABEL="Client Secret" +JLIB_FORM_FIELD_PARAM_OAUTH_CALLBACK_LABEL="Callback URL" JLIB_FORM_FIELD_PARAM_RADIO_MULTIPLE_DESC="Allow multiple values to be selected." JLIB_FORM_FIELD_PARAM_RADIO_MULTIPLE_LABEL="Multiple" JLIB_FORM_FIELD_PARAM_RADIO_MULTIPLE_VALUES_DESC="The options of the list." diff --git a/libraries/src/Form/Field/OAuth2ClientField.php b/libraries/src/Form/Field/OAuth2ClientField.php new file mode 100644 index 0000000000000..b01befa5e1464 --- /dev/null +++ b/libraries/src/Form/Field/OAuth2ClientField.php @@ -0,0 +1,216 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Form\Field; + +use Joomla\OAuth2\Client; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * The Field to load the form for OAuth2 client configuration inside current form + * + * @since __DEPLOY_VERSION__ + */ +class OAuth2ClientField extends SubformField +{ + /** + * The form field type. + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $type = 'OAuth2Client'; + + /** + * The callback url + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $callbackUrl; + + /** + * Layout to render + * + * @var string + * @since __DEPLOY_VERSION__ + */ + protected $layout; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since __DEPLOY_VERSION__ + */ + public function __get($name) + { + switch ($name) { + case 'callbackUrl': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function __set($name, $value) + { + switch ($name) { + case 'callbackUrl': + $this->$name = (string) $value; + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. + * + * @return boolean True on success. + * + * @since __DEPLOY_VERSION__ + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + /** + * When you have subforms which are not repeatable (i.e. a subform custom field with the + * repeat attribute set to 0) you get an array here since the data comes from decoding the + * JSON into an associative array, including the media subfield's data. + * + * However, this method expects an object or a string, not an array. Typecasting the array + * to an object solves the data format discrepancy. + */ + $value = \is_array($value) ? (object) $value : $value; + + /** + * If the value is not a string, it is + * most likely within a custom field of type subform + * and the value is a stdClass with properties + * imagefile and alt_text. So it is fine. + */ + // @todo + if (\is_string($value)) { + json_decode($value); + + // Check if value is a valid JSON string. + if ($value !== '' && json_last_error() !== JSON_ERROR_NONE) { + /** + * If the value is not empty and is not a valid JSON string, + * it is most likely a custom field created in Joomla 3 and + * the value is a string that contains the file name. + */ + if (is_file(JPATH_ROOT . '/' . $value)) { + $value = '{"imagefile":"' . $value . '","alt_text":""}'; + } else { + $value = ''; + } + } + } elseif ( + !\is_object($value) + || !property_exists($value, 'imagefile') + || !property_exists($value, 'alt_text') + ) { + return false; + } + + if (!parent::setup($element, $value, $group)) { + return false; + } + + $this->callbackUrl = (string) $this->element['callback_url']; + + $xml = << +
+
+ + + + + + + + + + + + + + +
+
+XML; + $this->formsource = $xml; + + $this->layout = 'joomla.form.field.subform.default'; + + return true; + } +} diff --git a/libraries/src/Mail/MailerOAuthTokenProvider.php b/libraries/src/Mail/MailerOAuthTokenProvider.php new file mode 100644 index 0000000000000..1223017d083c8 --- /dev/null +++ b/libraries/src/Mail/MailerOAuthTokenProvider.php @@ -0,0 +1,149 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Mail; + +use Joomla\CMS\Factory; +use Joomla\CMS\Log\Log; +use Joomla\Registry\Registry; +use PHPMailer\PHPMailer\OAuthTokenProvider; +use Joomla\OAuth2\Client; + +// phpcs:disable PSR1.Files.SideEffects +\defined('_JEXEC') or die; +// phpcs:enable PSR1.Files.SideEffects + +/** + * OAuth2 token provider wrapper class for phpmailer. + * Provides base64 encoded OAuth2 auth strings for SMTP authentication. + * + * @since __DEPLOY_VERSION__ + */ +abstract class MailerOAuthTokenProvider implements OAuthTokenProvider +{ + /** + * An instance of the League OAuth Client Provider. + * + * @var AbstractProvider + */ + protected $provider; + + /** + * The current OAuth access token. + * + * @var AccessToken + */ + protected $oauthToken; + + /** + * The user's email address, usually used as the login ID + * and also the from address when sending email. + * + * @var string + */ + protected $oauthUserEmail = ''; + + /** + * The client secret, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientSecret = ''; + + /** + * The client ID, generated in the app definition of the service you're connecting to. + * + * @var string + */ + protected $oauthClientId = ''; + + /** + * The refresh token, used to obtain new AccessTokens. + * + * @var string + */ + protected $oauthRefreshToken = ''; + + /** + * OAuth constructor. + * + * @param array $options Associative array containing + * `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements + */ + public function __construct($options) + { + $this->oauthUserEmail = $options['userName']; + $this->oauthClientSecret = $options['clientSecret']; + $this->oauthClientId = $options['clientId']; + $this->oauthRefreshToken = $options['refreshToken']; + + // $this->provider = new Client( + // [ + // 'redirectUri' => 'https://example.com/redirect', + // 'clientId' => $this->oauthClientId, + // 'clientSecret' => $this->oauthClientSecret, + // 'authurl' => 'https://example.com/auth', + // 'tokenurl' => 'https://example.com/token', + // ], + // null, Factory::getApplication()->getInput() + // ); + } + + /** + * Get a new RefreshToken. + * + * @return RefreshToken + */ + protected function getGrant() + { + // return new RefreshToken(); + return; + } + + /** + * Get a new AccessToken. + * + * @return AccessToken + */ + protected function getToken() + { + // return $this->provider->getAccessToken( + // $this->getGrant(), + // ['refresh_token' => $this->oauthRefreshToken] + // ); + return; + } + + + /** + * Generate a base64-encoded OAuth token ensuring that the access token has not expired. + * The string to be base 64 encoded should be in the form: + * "user=\001auth=Bearer \001\001" + * + * @return string + */ + public function getOauth64() { + + //Get a new token if it's not available or has expired + // if (null === $this->oauthToken || $this->oauthToken->hasExpired()) { + // $this->oauthToken = $this->getToken(); + // } + + // return base64_encode( + // 'user=' . + // $this->oauthUserEmail . + // "\001auth=Bearer " . + // $this->oauthToken . + // "\001\001" + // ); + + return ''; + } + +}