More Nextcloud changes #2055

This commit is contained in:
the-djmaze 2026-11-03 00:58:35 +01:00
parent 62c20c8327
commit c4f2033368
10 changed files with 228 additions and 30 deletions

View file

@ -28,13 +28,15 @@
* Custom 'data' folder path
*/
if (class_exists('OC')) {
define('APP_DATA_FOLDER_PATH', \OCA\SnappyMail\Util\SnappyMailHelper::getAppDataFolder());
/*
define('APP_DATA_FOLDER_PATH', \rtrim(\trim(
// \OCA\SnappyMail\Util\SnappyMailHelper::getSystemConfigValue('datadirectory', '')
\method_exists(\OC::$server, 'getSystemConfig')
? \OC::$server->getSystemConfig()->getValue('datadirectory', '')
// Nextcloud 34+
: \OC::$server->get(\OCP\IConfig::class)->getSystemValue('datadirectory', '')
), '\\/') . '/appdata_snappymail/');
*/
} else {
http_response_code(400);
header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request', true, 400);

View file

@ -68,7 +68,9 @@ There, click on the link to go to the SnappyMail admin panel.
<nextcloud min-version="25" />
</dependencies>
<settings>
<admin-section>OCA\SnappyMail\Settings\Section</admin-section>
<admin>OCA\SnappyMail\Settings\AdminSettings</admin>
<personal-section>OCA\SnappyMail\Settings\Section</personal-section>
<personal>OCA\SnappyMail\Settings\PersonalSettings</personal>
</settings>
<navigations>

View file

@ -75,7 +75,7 @@ class Application extends App implements IBootstrap
{
// SnappyMailHelper::$server = $context->getServerContainer();
if (!\is_dir(\rtrim(\trim(SnappyMailHelper::getSystemConfigValue('datadirectory', '')), '\\/') . '/appdata_snappymail')) {
if (!\is_dir(SnappyMailHelper::getAppDataFolder())) {
return;
}

View file

@ -10,17 +10,20 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
//use OCP\IUserSession;
class FetchController extends Controller
{
private IConfig $config;
private IL10N $l10n;
// private IUserSession $userSession;
public function __construct(string $appName, IRequest $request, \Psr\Container\ContainerInterface $container)
{
parent::__construct($appName, $request);
$this->config = $container->query(IConfig::class);
$this->l10n = $container->query(IL10N::class);
// $this->userSession = $container->query(IUserSession::class);
}
public function upgrade(): JSONResponse
@ -34,7 +37,7 @@ class FetchController extends Controller
'Message' => $this->l10n->t('Upgraded successfully')
]);
}
} catch (Exception $e) {
} catch (\Throwable $e) {
$error .= ': ' . $e->getMessage();
}
return new JSONResponse([
@ -91,7 +94,7 @@ class FetchController extends Controller
'status' => 'success',
'Message' => $this->l10n->t('Saved successfully')
]);
} catch (Exception $e) {
} catch (\Throwable $e) {
return new JSONResponse([
'status' => 'error',
'Message' => $e->getMessage()
@ -136,7 +139,7 @@ class FetchController extends Controller
'Message' => $this->l10n->t('Saved successfully'),
'Email' => $sEmail
]);
} catch (Exception $e) {
} catch (\Throwable $e) {
// Logout as the credentials might have changed, as exception could be in one attribute
// TODO: Handle both exceptions separately?
SnappyMailHelper::loadApp();

View file

@ -33,13 +33,18 @@ class PageController extends Controller
$bAdmin = false;
if (!empty($_SERVER['QUERY_STRING'])) {
SnappyMailHelper::loadApp();
$bAdmin = \RainLoop\Api::Config()->Get('security', 'admin_panel_key', 'admin') == $_SERVER['QUERY_STRING'];
$bAdmin = $this->getAdminPanelKey() == $_SERVER['QUERY_STRING'];
if ($this->isAdminServiceRequest()) {
$this->ensureNextcloudAdminSession();
}
if (!$bAdmin) {
return SnappyMailHelper::startApp(true);
}
}
if (!$bAdmin && SnappyMailHelper::getAppConfigValue('snappymail-no-embed')) {
if (!$bAdmin && $this->config->getAppValue('snappymail', 'snappymail-no-embed')) {
// if (!$bAdmin && SnappyMailHelper::getAppConfigValue('snappymail-no-embed')) {
// SnappyMailHelper::getNavigationManager()->setActiveEntry('snappymail');
$this->navigationManager->setActiveEntry('snappymail');
\OCP\Util::addScript('snappymail', 'snappymail');
\OCP\Util::addStyle('snappymail', 'style');
@ -51,10 +56,119 @@ class PageController extends Controller
$csp = new ContentSecurityPolicy();
$csp->addAllowedFrameDomain("'self'");
// $csp->addAllowedFrameAncestorDomain("'self'");
$response->setContentSecurityPolicy($csp);
$response->setContentSecurityPolicy($csp->getPolicy());
return $response;
}
return $this->renderEmbeddedApp($bAdmin);
}
/**
* Integrated administration entry point.
*
* Unlike the legacy ?admin URL this route is protected by Nextcloud's
* default admin middleware. No @NoAdminRequired annotation must be added.
*
* @NoCSRFRequired
*/
public function admin()
{
$this->ensureNextcloudAdminSession();
if (!empty($_SERVER['QUERY_STRING'])) {
return $this->handleIntegratedAdminRequest();
}
return $this->renderEmbeddedApp(true, true);
}
/**
* SnappyMail uses its own X-SM-Token for the legacy admin JSON API. The
* Nextcloud CSRF middleware cannot validate that token, therefore this
* endpoint opts out of the Nextcloud request token check. It remains admin
* protected and SnappyMail validates X-SM-Token for every modifying call.
*
* @NoCSRFRequired
*/
public function adminPost()
{
$this->ensureNextcloudAdminSession();
return $this->handleIntegratedAdminRequest();
}
private function ensureNextcloudAdminSession(): void
{
$user = \OC::$server->get(\OCP\IUserSession::class)->getUser();
if (!$user || !\OC_User::isAdminUser($user->getUID())) {
throw new \RuntimeException('SnappyMail administration requires a Nextcloud administrator session.');
}
SnappyMailHelper::loadApp();
$config = \RainLoop\Api::Config();
if (!$config->Get('security', 'allow_admin_panel', true)) {
throw new \RuntimeException('SnappyMail administration is disabled.');
}
$actions = \RainLoop\Api::Actions();
if (!$actions->IsAdminLoggined(false)) {
$random = \MailSo\Base\Utils::Sha1Rand();
if (!$actions->Cacher(null, true)->Set(\RainLoop\KeyPathHelper::SessionAdminKey($random), \time())) {
throw new \RuntimeException('Could not create the SnappyMail administrator session.');
}
$token = \RainLoop\Utils::EncodeKeyValuesQ(['token', $random]);
if (!$token) {
throw new \RuntimeException('Could not encode the SnappyMail administrator session.');
}
\SnappyMail\Cookies::set('smadmin', $token);
}
}
private function handleIntegratedAdminRequest(): void
{
$adminKey = $this->getAdminPanelKey();
$query = \trim((string) ($_SERVER['QUERY_STRING'] ?? ''), " \t\n\r\0\x0B/");
// The SnappyMail admin client already includes its configured admin
// path in JSON and asset requests. Prefix only requests that do not
// contain it yet; otherwise "admin/admin/Json" is produced and domain
// as well as extension requests are routed to a non-existing service.
if ($query !== $adminKey && 0 !== \strpos($query, $adminKey . '/')) {
$query = $adminKey . ($query ? '/' . $query : '');
}
$_SERVER['QUERY_STRING'] = $query;
$_GET[$adminKey] = '1';
SnappyMailHelper::startApp(true);
}
private function getAdminPanelKey(): string
{
$key = (string) \RainLoop\Api::Config()->Get('admin_panel', 'key', '');
return '' !== $key ? $key : 'admin';
}
private function isAdminServiceRequest(): bool
{
$query = \trim((string) ($_SERVER['QUERY_STRING'] ?? ''), " \t\n\r\0\x0B/");
$ampersand = \strpos($query, '&');
if (false !== $ampersand) {
$query = \substr($query, 0, $ampersand);
}
$adminKey = $this->getAdminPanelKey();
return $query === $adminKey || 0 === \strpos($query, $adminKey . '/');
}
private function protectLegacyAdminRequest(): void
{
SnappyMailHelper::loadApp();
if ($this->isAdminServiceRequest()) {
$this->ensureNextcloudAdminSession();
}
}
private function renderEmbeddedApp(bool $bAdmin, bool $blank = false)
{
// SnappyMailHelper::getNavigationManager()->setActiveEntry('snappymail');
$this->navigationManager->setActiveEntry('snappymail');
\OCP\Util::addStyle('snappymail', 'embed');
@ -93,9 +207,9 @@ class PageController extends Controller
\OCP\Util::addHeader('link', ['type'=>'text/css','rel'=>'stylesheet','href'=>\RainLoop\Utils::WebStaticPath('css/'.($bAdmin?'admin':'app').$sAppCssMin.'.css')], '');
// \OCP\Util::addHeader('style', ['id'=>'app-theme-style','data-href'=>$params['BaseAppThemeCssLink']], $params['BaseAppThemeCss']);
$response = new TemplateResponse('snappymail', 'index_embed', $params);
$response = new TemplateResponse('snappymail', 'index_embed', $params, $blank ? 'blank' : 'user');
$response->setContentSecurityPolicy($csp);
$response->setContentSecurityPolicy($csp->getPolicy());
return $response;
}
@ -106,6 +220,7 @@ class PageController extends Controller
*/
public function appGet()
{
$this->protectLegacyAdminRequest();
return SnappyMailHelper::startApp(true);
}
@ -115,6 +230,7 @@ class PageController extends Controller
*/
public function appPost()
{
$this->protectLegacyAdminRequest();
return SnappyMailHelper::startApp(true);
}
@ -124,6 +240,7 @@ class PageController extends Controller
*/
public function indexPost()
{
$this->protectLegacyAdminRequest();
return SnappyMailHelper::startApp(true);
}
}

View file

@ -68,6 +68,7 @@ class AdminSettings implements ISettings
$parameters['snappymail-admin-password'] = $sPassword;
$parameters['can-import-rainloop'] = $sPassword && \is_dir(
// SnappyMailHelper::getAppDataFolder()
\rtrim(\trim($this->config->getSystemValue('datadirectory', '')), '\\/')
. '/rainloop-storage'
);
@ -98,7 +99,7 @@ class AdminSettings implements ISettings
public function getSection()
{
return 'additional';
return 'snappymail';
}
public function getPriority()

View file

@ -8,7 +8,7 @@ use OCP\Settings\ISettings;
class PersonalSettings implements ISettings
{
private $config;
private IConfig $config;
public function __construct(IConfig $config)
{
@ -33,7 +33,7 @@ class PersonalSettings implements ISettings
public function getSection()
{
return 'additional';
return 'snappymail';
}
public function getPriority()

View file

@ -0,0 +1,38 @@
<?php
namespace OCA\SnappyMail\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class Section implements IIconSection
{
private IL10N $l;
private IURLGenerator $urlGenerator;
public function __construct(IL10N $l, IURLGenerator $urlGenerator)
{
$this->l = $l;
$this->urlGenerator = $urlGenerator;
}
public function getID(): string
{
return 'snappymail';
}
public function getName(): string
{
return $this->l->t('SnappyMail');
}
public function getPriority(): int
{
return 50;
}
public function getIcon(): string
{
return $this->urlGenerator->imagePath('snappymail', 'favicon-mask.svg');
}
}

View file

@ -15,9 +15,8 @@ class RainLoop
*/
public static function import(): array
{
$dir = \rtrim(\trim(SnappyMailHelper::getSystemConfigValue('datadirectory', '')), '\\/');
$dir_snappy = $dir . '/appdata_snappymail/';
$dir_rainloop = $dir . '/rainloop-storage';
$dir_snappy = SnappyMailHelper::getAppDataFolder();
$dir_rainloop = SnappyMailHelper::getDataDirectory() . '/rainloop-storage';
$result = [];
$rainloop_plugins = [];
if (\is_dir($dir_rainloop)) {

View file

@ -34,6 +34,16 @@ class SnappyMailHelper
return static::$server || \OC::$server;
}
public static function getAppDataFolder(): string
{
return static::getDataDirectory() . '/appdata_snappymail/';
}
public static function getDataDirectory(): string
{
return \rtrim(\trim(static::getSystemConfigValue('datadirectory', '')), '\\/');
}
public static function getConfig(): object
{
return \method_exists(\OC::$server, 'getConfig')
@ -110,10 +120,26 @@ class SnappyMailHelper
}
}
public static function linkToRoute(string $value): string
{
return (\method_exists(\OC::$server, 'getURLGenerator')
? \OC::$server->getURLGenerator()
// Nextcloud 34+
: \OCP\Server::get(\OCP\IURLGenerator::class)
)->linkToRoute($value);
}
public static function loadApp(): void
{
$app_dir = \dirname(\dirname(__DIR__)) . '/app';
if (\class_exists('RainLoop\\Api')) {
return;
$expected = \realpath($app_dir);
$actual = \defined('APP_INDEX_ROOT_PATH') ? \realpath(APP_INDEX_ROOT_PATH) : false;
if ($expected && $actual && $expected === $actual) {
return;
}
throw new \RuntimeException('SnappyMail cannot start because another SnappyMail-compatible app is already loaded in the same request.');
}
// Nextcloud the default spl_autoload_register() not working
@ -129,9 +155,8 @@ class SnappyMailHelper
// define('APP_VERSION', '0.0.0');
// define('APP_INDEX_ROOT_PATH', __DIR__ . DIRECTORY_SEPARATOR);
// include APP_INDEX_ROOT_PATH.'snappymail/v/'.APP_VERSION.'/include.php';
// define('APP_DATA_FOLDER_PATH', \rtrim(\trim(static::getSystemConfigValue('datadirectory', '')), '\\/').'/appdata_snappymail/');
// define('APP_DATA_FOLDER_PATH', static::getAppDataFolder());
$app_dir = \dirname(\dirname(__DIR__)) . '/app';
require_once $app_dir . '/index.php';
}
@ -173,7 +198,7 @@ class SnappyMailHelper
if (24 < $OC_Version[0]) {
$ocSession = static::getSession();
$ocSession->reopen();
if (!$doLogin && $ocSession['snappymail-uid'] && $ocSession['snappymail-uid'] != $aCredentials[0]) {
if (!$doLogin && $ocSession->get('snappymail-uid') && $ocSession->get('snappymail-uid') != $aCredentials[0]) {
// UID changed, Impersonate plugin probably active
$oActions->Logout(true);
$doLogin = true;
@ -193,9 +218,20 @@ class SnappyMailHelper
} catch (\Throwable $e) {
// Login failure, reset password to prevent more attempts
if (!$isOIDC) {
$sUID = static::getUID();
static::setSessionValue('snappymail-passphrase', '');
static::getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
// Credentials saved in the user's personal settings must never be
// deleted as a side effect of a failed login. A temporary network,
// DNS, TLS, or mail server outage is not a password change.
//
// Only discard the password captured from the current Nextcloud
// session when the IMAP server explicitly rejected authentication.
// Persisted credentials remain available for the user to inspect or
// replace in their personal settings.
if ($e instanceof \RainLoop\Exceptions\ClientException
&& \RainLoop\Notifications::AuthError === $e->getCode()
) {
static::setSessionValue('snappymail-passphrase', '');
// static::getConfig()->setUserValue(static::getUID(), 'snappymail', 'passphrase', '');
}
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
}
}
@ -261,7 +297,7 @@ class SnappyMailHelper
// If the current user ID is identical to login ID (not valid when using account switching),
// this has the second priority.
if ($ocSession['snappymail-nc-uid'] == $sUID) {
if ($ocSession->get('snappymail-nc-uid') == $sUID) {
// If OpenID Connect (OIDC) is enabled and used for login, use this.
if (static::isOIDCLogin()) {
@ -276,10 +312,10 @@ class SnappyMailHelper
$sPassword = '';
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
$sEmail = $sUID;
$sPassword = $ocSession['snappymail-passphrase'];
$sPassword = $ocSession->get('snappymail-passphrase');
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
$sPassword = $ocSession['snappymail-passphrase'];
$sPassword = $ocSession->get('snappymail-passphrase');
} else {
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
}
@ -287,7 +323,7 @@ class SnappyMailHelper
return [$sUID, $sEmail, static::decodePassword($sPassword, $sUID)];
}
} else {
\SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession['snappymail-nc-uid']}' != '{$sUID}'");
\SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession->get('snappymail-nc-uid')}' != '{$sUID}'");
}
return [$sUID, '', ''];
@ -295,7 +331,7 @@ class SnappyMailHelper
public static function getAppUrl(): string
{
return \OC::$server->getURLGenerator()->linkToRoute('snappymail.page.appGet');
return static::linkToRoute('snappymail.page.appGet');
}
public static function normalizeUrl(string $sUrl): string
@ -308,13 +344,13 @@ class SnappyMailHelper
return $sUrl;
}
public static function encodePassword(string $sPassword, string $sSalt): string
public static function encodePassword(#[\SensitiveParameter] string $sPassword, string $sSalt): string
{
static::loadApp();
return \SnappyMail\Crypt::EncryptUrlSafe($sPassword, $sSalt);
}
public static function decodePassword(string $sPassword, string $sSalt): ?\SnappyMail\SensitiveString
public static function decodePassword(#[\SensitiveParameter] string $sPassword, string $sSalt): ?\SnappyMail\SensitiveString
{
static::loadApp();
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);