HEX
Server: Apache
System: Linux WWW 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: web11 (1011)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/apklausos/application/core/plugins/AzureOAuthSMTP/AzureOAuthSMTP.php
<?php

use LimeSurvey\PluginManager\SmtpOAuthPluginBase;
use PHPMailer\PHPMailer\PHPMailer;

class AzureOAuthSMTP extends SmtpOAuthPluginBase
{
    protected $storage = 'DbStorage';
    protected static $description = 'Core: Adds Azure OAuth support for email sending';
    protected static $name = 'AzureOAuthSMTP';

    /** @inheritdoc this plugin doesn't have any public method */
    public $allowedPublicMethods = [];

    /** @inheritdoc */
    protected $credentialAttributes = ['clientId', 'clientSecret', 'tenantId'];

    /** @inheritdoc */
    protected $encryptedSettings = ['clientSecret'];

    protected $settings = [
        'help' => [
            'type' => 'info',
            'content' => '',
        ],
        'clientId' => [
            'type' => 'string',
            'label' => 'Client ID',
        ],
        'clientSecret' => [
            'type' => 'string',
            'label' => 'Client Secret',
        ],
        'tenantId' => [
            'type' => 'string',
            'label' => 'Tenant ID',
        ],
    ];

    public function init()
    {
        // This plugin is only compatible with PHP 7.3 and above,
        // while LimeSurvey is still compatible with PHP 7.2, so
        // we need to make sure we only load composer's autoload
        // on PHP 7.3 and above. We also need to prevent activation
        // of the plugin on PHP 7.2 and below.
        // TODO: Remove this once we drop support for PHP 7.2
        $this->subscribe('beforeActivate');
        if (!(PHP_VERSION_ID >= 70300)) {
            return;
        }
        require_once __DIR__ . '/vendor/autoload.php';

        $this->subscribe('listEmailPlugins');
        $this->subscribe('afterSelectEmailPlugin');
        $this->subscribe('MailerConstruct');    // Handler defined in SmtpOAuthPluginBase
        $this->subscribe('beforePrepareRedirectToAuthPage');
        $this->subscribe('beforeRedirectToAuthPage');   // Handler defined in SmtpOAuthPluginBase
        $this->subscribe('afterReceiveOAuthResponse');  // Handler defined in SmtpOAuthPluginBase

        $this->subscribe('beforeEmailDispatch');
    }

    /**
     * TODO: Remove this once we drop support for PHP 7.2
     */
    public function beforeActivate()
    {
        if (!(PHP_VERSION_ID >= 70300)) {
            $event = $this->getEvent();
            $event->set('success', false);
            $event->set('message', gT("This plugin requires PHP version 7.3 or higher."));
        }
    }

    /**
     * @inheritdoc
     * Update the information content
     */
    public function getPluginSettings($getValues = true)
    {
        $settings = parent::getPluginSettings($getValues);
        $settings['help']['content'] = $this->getHelpContent();
        $settings['clientId']['label'] = gT("Client ID");
        $settings['clientSecret']['label'] = gT("Client Secret");
        $settings['tenantId']['label'] = gT("Tenant ID");

        return $settings;
    }

    private function getHelpContent()
    {
        $this->subscribe('getPluginTwigPath');
        $data = [
            'redirectUri' => $this->getRedirectUri(),
            'isHttps' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'),
        ];
        return Yii::app()->twigRenderer->renderPartial('/Help.twig', $data);

        // Translations here just so the translations bot can pick them up.
        $lang = [
            gT("Help"),
            gT("Prerequisites:"),
            gT("Access LimeSurvey over HTTPS."),
            gT("Currently not served over HTTPS"),
            gT("Instructions:"),
        ];
    }

    /**
     * @inheritdoc
     */
    protected function getProvider($credentials)
    {
        $redirectUri = $this->getRedirectUri();
        $params = [
            'clientId' => $credentials['clientId'],
            'clientSecret' => $credentials['clientSecret'],
            'tenantId' => $credentials['tenantId'],
            'redirectUri' => $redirectUri,
            'accessType' => 'offline',
            'prompt' => 'consent',
        ];
        /**
         * @psalm-suppress UndefinedClass
         */
        return new Greew\OAuth2\Client\Provider\Azure($params);
    }

    /**
     * Adds view's path to twig system
     */
    public function getPluginTwigPath()
    {
        $viewPath = dirname(__FILE__) . "/views";
        $this->getEvent()->append('add', array($viewPath));
    }

    /**
     * Adds the plugin to the list of email plugins
     */
    public function listEmailPlugins()
    {
        $event = $this->getEvent();
        $event->append('plugins', [
            'azure' => $this->getEmailPluginInfo()
        ]);
    }

    /**
     * @inheritdoc
     */
    public function getOAuthConfigForMailer()
    {
        try {
            $credentials = $this->getCredentials();
            $clientId = $credentials['clientId'];
            $clientSecret = $credentials['clientSecret'];
        } catch (Exception $ex) {
            $this->log("AzureOAuthSMTP is enabled but credentials are incomplete.", CLogger::LEVEL_WARNING);
            return;
        }

        $refreshToken = $this->get("refreshToken");
        if (empty($refreshToken)) {
            $this->log("AzureOAuthSMTP is enabled but there is no refresh token stored.", CLogger::LEVEL_WARNING);
            return;
        }

        $emailAddress = $this->get("email");
        if (empty($emailAddress)) {
            $this->log("AzureOAuthSMTP is enabled but there is no email address. Please generate a new token.", CLogger::LEVEL_WARNING);
            return;
        }

        $provider = $this->getProvider($credentials);
        $config = [
            'provider' => $provider,
            'clientId' => $clientId,
            'clientSecret' => $clientSecret,
            'refreshToken' => $refreshToken,
            'userName' => $emailAddress,
        ];

        return $config;
    }

    /**
     * @inheritdoc
     * Add Azure specific settings to the mailer
     */
    protected function setupMailer($mailer)
    {
        parent::setupMailer($mailer);
        $mailer->Host = 'smtp.office365.com';
        $mailer->Port = 587;
        $mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    }

    /**
     * Handles the afterSelectEmailPlugin event, triggered when the plugin
     * is selected as email plugin in Global Settings
     */
    public function afterSelectEmailPlugin()
    {
        $setupStatus = $this->getSetupStatus();
        if ($setupStatus !== self::SETUP_STATUS_VALID_REFRESH_TOKEN) {
            $event = $this->getEvent();
            $event->set('warning', sprintf(gT("The %s plugin is not configured correctly. Please check the plugin settings."), self::getName()));
        }
    }

    /**
     * @inheritdoc
     */
    protected function getAuthorizationOptions()
    {
        return [
            'scope' => [
                'https://outlook.office.com/SMTP.Send openid email offline_access ',
            ]
        ];
    }

    /**
     * Handles the beforeEmailDispatch event, triggered right before an email is sent.
     * This is used to override the sender with the logged user, and to set the "Reply To" header,
     * because Azure doesn't accept the sender to be different from the logged user.
     */
    public function beforeEmailDispatch()
    {
        // Don't do anything if the current plugin is not the one selected.
        if (!$this->isCurrentEmailPlugin()) {
            return;
        }

        $event = $this->getEvent();

        /** @var LimeMailer */
        $limeMailer = $event->get('mailer');
        // Set "Reply To" because we need to override the From/Sender with the logged user.
        $limeMailer->AddReplyTo($limeMailer->From, $limeMailer->FromName);

        // Override the sender to avoid to match OAuth credentials.
        $from = $this->get('email');
        $limeMailer->setFrom($from);
    }

    /**
     * @inheritdoc
     */
    protected function afterRefreshTokenRetrieved($provider, $token)
    {
        $tokenValues = $token->getValues();
        if (empty($tokenValues['id_token'])) {
            throw new Exception("The token doesn't contain an id_token. This is required to get the user's email address.");
        }
        $idToken = $tokenValues['id_token'];
        $idTokenParts = explode('.', $idToken);
        $idTokenPayload = $idTokenParts[1];
        $decodedToken = json_decode(base64_decode($idTokenPayload));
        $email = $decodedToken->email;
        $this->set('email', $email);
    }

     /**
     * @inheritdoc
     */
    protected function getDisplayName()
    {
        return 'Azure';
    }

    /**
     * Handles the beforePrepareRedirectToAuthPage event, triggered before the
     * page with the "Get Token" button is rendered.
     */
    public function beforePrepareRedirectToAuthPage()
    {
        $event = $this->getEvent();
        $event->set('width', 600);
        $event->set('height', 800);
        $event->set('providerName', $this->getDisplayName());

        $setupStatus = $this->getSetupStatus();
        $description = $this->getSetupStatusDescription($setupStatus);
        $event->setContent($this, $description);
    }

    /**
     * @inheritdoc
     */
    protected function getSetupStatusAlert()
    {
        if (Yii::app()->getUrlManager()->getUrlFormat() == CUrlManager::GET_FORMAT) {
            return "<div class=\"alert alert-danger\">" . gT("Azure doesn't accept redirect URIs with query parameters when using personal accounts. This plugin will not work properly with the current URL manager configuration.") . "</div>";
        }

        return parent::getSetupStatusAlert();
    }
}