mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-09-20 07:35:55 +08:00
Compare commits
18 commits
e6735f1256
...
a40ef74167
Author | SHA1 | Date | |
---|---|---|---|
a40ef74167 | |||
cfbc47488a | |||
05812c6be1 | |||
18452cc53c | |||
e0236ea52d | |||
6f4f6bfd03 | |||
f8520c27a2 | |||
61e7c00b62 | |||
ed41d8e45f | |||
80f3331187 | |||
9a7ff9fe27 | |||
12b6ed3dbb | |||
48acb9ca38 | |||
deae36d8c8 | |||
947ce59eb9 | |||
6e7d024b40 | |||
26c1bc13c0 | |||
68720f283d |
|
@ -17,14 +17,34 @@ const
|
|||
"'": '''
|
||||
},
|
||||
|
||||
disallowedTags = [
|
||||
'svg','script','title','link','base','meta',
|
||||
'input','output','select','button','textarea',
|
||||
'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map'
|
||||
// not supported by <template> element
|
||||
// ,'html','head','body'
|
||||
keepTagContent = 'form,button,data', // font
|
||||
|
||||
allowedTags = [
|
||||
// Structural Elements:
|
||||
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
|
||||
'article','aside','header','footer','main','section',
|
||||
'details','summary',
|
||||
// List Elements
|
||||
'dd','dl','dt','li','ol','ul',
|
||||
// Text Formatting Elements
|
||||
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
|
||||
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
|
||||
'span','strong','sub','sup','time','u','var',
|
||||
// Deprecated by HTML Standard
|
||||
'acronym','big','center','dir','font','marquee',
|
||||
'nobr','noembed','noframes','plaintext','rb','rtc','strike','tt',
|
||||
// Media Elements
|
||||
'img',//'picture','source',
|
||||
// Table Elements
|
||||
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
|
||||
// Disallowed but converted later
|
||||
'style','xmp'
|
||||
].join(','),
|
||||
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG'
|
||||
],
|
||||
|
||||
blockquoteSwitcher = () => {
|
||||
SettingsUserStore.collapseBlockquotes() &&
|
||||
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
|
||||
|
@ -102,8 +122,10 @@ const
|
|||
},
|
||||
|
||||
cleanCSS = source =>
|
||||
source.trim().replace(/(^|;)\s*-(ms|webkit)-[^;]+(;|$)/g, '')
|
||||
.replace(/white-space[^;]+(;|$)/g, '')
|
||||
source.trim()
|
||||
.replace(/;\s*-[^;]+/g, '')
|
||||
.replace(/^\s*-[^;]+(;|$)/g, '')
|
||||
.replace(/white-space[^;]+/g, '')
|
||||
// Drop Microsoft Office style properties
|
||||
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
||||
,
|
||||
|
@ -145,14 +167,14 @@ const
|
|||
if (source) {
|
||||
source = source
|
||||
// strip comments
|
||||
.replace(/\/\*[\s\S]*?\*\/|<!--|-->/gi, '')
|
||||
// strip import statements
|
||||
.replace(/@import .*?;/gi , '')
|
||||
// strip keyframe statements
|
||||
.replace(/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi, '');
|
||||
.replace(/\/\*[\s\S]*?\*\//gi, '')
|
||||
// strip MS Word comments
|
||||
.replace(/<!--[\s\S]*?-->/gi, '');
|
||||
// strip HTML
|
||||
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
|
||||
|
||||
// unified regex to match css & media queries together
|
||||
let unified = /((\s*?(?:\/\*[\s\S]*?\*\/)?\s*?@media[\s\S]*?){([\s\S]*?)}\s*?})|(([\s\S]*?){([\s\S]*?)})/gi,
|
||||
let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
|
||||
arr;
|
||||
|
||||
while (true) {
|
||||
|
@ -161,7 +183,7 @@ const
|
|||
break;
|
||||
}
|
||||
|
||||
let selector = arr[arr[2] === undefined ? 5 : 2].split('\r\n').join('\n').trim()
|
||||
let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
|
||||
// Never have more than a single line break in a row
|
||||
.replace(/\n+/, "\n")
|
||||
// Remove :root and html
|
||||
|
@ -173,13 +195,14 @@ const
|
|||
css.push({
|
||||
selector: selector,
|
||||
type: 'media',
|
||||
subStyles: parseCSS(arr[3] + '\n}') //recursively parse media query inner css
|
||||
subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
|
||||
});
|
||||
} else if (selector && !selector.includes('@')) {
|
||||
// we have standard css
|
||||
// ignores @import, @keyframe, @font-face statements
|
||||
css.push({
|
||||
selector: selector,
|
||||
rules: cleanCSS(arr[6])
|
||||
rules: cleanCSS(arr[4])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -258,9 +281,6 @@ export const
|
|||
'abbr', 'scope',
|
||||
// td
|
||||
'colspan', 'rowspan', 'headers'
|
||||
],
|
||||
nonEmptyTags = [
|
||||
'A','B','EM','I','SPAN','STRONG'
|
||||
];
|
||||
|
||||
if (SettingsUserStore.allowStyles()) {
|
||||
|
@ -307,13 +327,21 @@ export const
|
|||
}
|
||||
});
|
||||
|
||||
// https://github.com/the-djmaze/snappymail/issues/1125
|
||||
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
|
||||
|
||||
tmpl.content.querySelectorAll(
|
||||
disallowedTags
|
||||
':not('+allowedTags+')'
|
||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||
).forEach(oElement => oElement.remove());
|
||||
|
||||
// https://github.com/the-djmaze/snappymail/issues/1125
|
||||
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
|
||||
/* // Is this slower or faster?
|
||||
).forEach(oElement => {
|
||||
if (!node || !node.contains(oElement)) {
|
||||
oElement.remove();
|
||||
node = oElement;
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// https://github.com/the-djmaze/snappymail/issues/1641
|
||||
let body = tmpl.content.querySelector('.mail-body');
|
||||
|
@ -346,6 +374,13 @@ export const
|
|||
return;
|
||||
}
|
||||
|
||||
if ('XMP' === name) {
|
||||
const pre = createElement('pre');
|
||||
pre.innerHTML = encodeHtml(oElement.innerHTML);
|
||||
oElement.replaceWith(pre);
|
||||
return;
|
||||
}
|
||||
|
||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
||||
if ('none' == oStyle.display
|
||||
|| 'hidden' == oStyle.visibility
|
||||
|
@ -432,7 +467,7 @@ export const
|
|||
}
|
||||
|
||||
// if (['CENTER','FORM'].includes(name)) {
|
||||
if ('O:P' === name || (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim()))) {
|
||||
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
|
||||
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
|
||||
return;
|
||||
}
|
||||
|
|
19
dev/External/SquireUI.js
vendored
19
dev/External/SquireUI.js
vendored
|
@ -155,7 +155,7 @@ class SquireUI
|
|||
html: 'B',
|
||||
cmd: () => this.doAction('bold'),
|
||||
key: 'B',
|
||||
matches: 'B,STRONT'
|
||||
matches: 'B,STRONG'
|
||||
},
|
||||
italic: {
|
||||
html: 'I',
|
||||
|
@ -477,15 +477,22 @@ class SquireUI
|
|||
|
||||
// -----
|
||||
|
||||
squire.addEventListener('pathChange', e => {
|
||||
squire.addEventListener('pathChange', () => {
|
||||
|
||||
const squireRoot = squire.getRoot();
|
||||
let elm = e.detail.element;
|
||||
|
||||
let range = squire.getSelection(),
|
||||
collapsed = range.collapsed,
|
||||
elm = collapsed ? range.endContainer : range?.commonAncestorContainer;
|
||||
if (elm && !(elm instanceof Element)) {
|
||||
elm = elm.parentElement;
|
||||
}
|
||||
forEachObjectValue(actions, entries => {
|
||||
forEachObjectValue(entries, cfg => {
|
||||
// cfg.matches && cfg.input.classList.toggle('active', elm && elm.matches(cfg.matches));
|
||||
cfg.matches && cfg.input.classList.toggle('active', elm && elm.closestWithin(cfg.matches, squireRoot));
|
||||
// Check if selection has a matching parent or contains a matching element
|
||||
cfg.matches && cfg.input.classList.toggle('active', !!(elm && (
|
||||
(!collapsed && [...elm.querySelectorAll(cfg.matches)].some(node => range.intersectsNode(node)))
|
||||
|| elm.closestWithin(cfg.matches, squireRoot)
|
||||
)));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -90,26 +90,23 @@ class SnappyMailHelper
|
|||
}
|
||||
*/
|
||||
if ($doLogin && $aCredentials[1] && $aCredentials[2]) {
|
||||
$isOIDC = \str_starts_with($aCredentials[2], 'oidc_login|');
|
||||
try {
|
||||
$ocSession = \OC::$server->getSession();
|
||||
if ($ocSession->get('is_oidc')) {
|
||||
$pwd = new \SnappyMail\SensitiveString($aCredentials[1]);
|
||||
$oAccount = $oActions->LoginProcess($aCredentials[1], $pwd);
|
||||
if ($oAccount) {
|
||||
$oActions->SetSignMeToken($oAccount);
|
||||
}
|
||||
} else {
|
||||
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
|
||||
if ($oAccount && $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) {
|
||||
$oActions->SetSignMeToken($oAccount);
|
||||
}
|
||||
$oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]);
|
||||
if (!$isOIDC && $oAccount
|
||||
&& $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn
|
||||
) {
|
||||
$oActions->SetSignMeToken($oAccount);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Login failure, reset password to prevent more attempts
|
||||
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
||||
\OC::$server->getSession()['snappymail-passphrase'] = '';
|
||||
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
|
||||
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
|
||||
if (!$isOIDC) {
|
||||
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
||||
\OC::$server->getSession()['snappymail-passphrase'] = '';
|
||||
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
|
||||
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +123,32 @@ class SnappyMailHelper
|
|||
}
|
||||
}
|
||||
|
||||
// Check if OpenID Connect (OIDC) is enabled and used for login
|
||||
// https://apps.nextcloud.com/apps/oidc_login
|
||||
public static function isOIDCLogin() : bool
|
||||
{
|
||||
$config = \OC::$server->getConfig();
|
||||
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
|
||||
// Check if the OIDC Login app is enabled
|
||||
if (\OC::$server->getAppManager()->isEnabledForUser('oidc_login')) {
|
||||
// Check if session is an OIDC Login
|
||||
$ocSession = \OC::$server->getSession();
|
||||
if ($ocSession->get('is_oidc')) {
|
||||
// IToken->getPassword() ???
|
||||
if ($ocSession->get('oidc_access_token')) {
|
||||
return true;
|
||||
}
|
||||
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
|
||||
} else {
|
||||
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
|
||||
}
|
||||
} else {
|
||||
\SnappyMail\Log::debug('Nextcloud', 'OIDC login disabled');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function getLoginCredentials() : array
|
||||
{
|
||||
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
|
||||
|
@ -151,18 +174,9 @@ class SnappyMailHelper
|
|||
if ($ocSession['snappymail-nc-uid'] == $sUID) {
|
||||
|
||||
// If OpenID Connect (OIDC) is enabled and used for login, use this.
|
||||
// https://apps.nextcloud.com/apps/oidc_login
|
||||
if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) {
|
||||
if ($ocSession->get('is_oidc')) {
|
||||
// IToken->getPassword() ???
|
||||
if ($sAccessToken = $ocSession->get('oidc_access_token')) {
|
||||
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
|
||||
return [$sUID, $sEmail, $sAccessToken];
|
||||
}
|
||||
\SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing');
|
||||
} else {
|
||||
\SnappyMail\Log::debug('Nextcloud', 'No OIDC login');
|
||||
}
|
||||
if (static::isOIDCLogin()) {
|
||||
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
|
||||
return [$sUID, $sEmail, "oidc_login|{$sUID}"];
|
||||
}
|
||||
|
||||
// Only use the user's password in the current session if they have
|
||||
|
|
|
@ -47,7 +47,10 @@ class Memcache implements \MailSo\Cache\DriverInterface
|
|||
|
||||
public function Set(string $sKey, string $sValue) : bool
|
||||
{
|
||||
return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire) : false;
|
||||
if ($this->oMem instanceof \Memcache) {
|
||||
return $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire);
|
||||
}
|
||||
return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, $this->iExpire) : false;
|
||||
}
|
||||
|
||||
public function Exists(string $sKey) : bool
|
||||
|
|
|
@ -4,8 +4,8 @@ class CacheMemcachePlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Cache Memcache',
|
||||
VERSION = '2.36',
|
||||
RELEASE = '2024-03-22',
|
||||
VERSION = '2.37',
|
||||
RELEASE = '2024-09-15',
|
||||
REQUIRED = '2.36.0',
|
||||
CATEGORY = 'Cache',
|
||||
DESCRIPTION = 'Cache handler using PHP Memcache or PHP Memcached';
|
||||
|
|
|
@ -4,8 +4,8 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
const
|
||||
NAME = 'Nextcloud',
|
||||
VERSION = '2.37.0',
|
||||
RELEASE = '2024-08-11',
|
||||
VERSION = '2.37.1',
|
||||
RELEASE = '2024-09-16',
|
||||
CATEGORY = 'Integrations',
|
||||
DESCRIPTION = 'Integrate with Nextcloud v20+',
|
||||
REQUIRED = '2.36.2';
|
||||
|
@ -35,6 +35,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
|
||||
$this->addTemplate('templates/PopupsNextcloudFiles.html');
|
||||
$this->addTemplate('templates/PopupsNextcloudCalendars.html');
|
||||
$this->addTemplate('templates/PopupsNextcloudInvites.html');
|
||||
|
||||
// $this->addHook('login.credentials.step-2', 'loginCredentials2');
|
||||
// $this->addHook('login.credentials', 'loginCredentials');
|
||||
|
@ -89,32 +90,17 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
|
||||
public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void
|
||||
{
|
||||
// https://apps.nextcloud.com/apps/oidc_login
|
||||
$config = \OC::$server->getConfig();
|
||||
$oUser = \OC::$server->getUserSession()->getUser();
|
||||
$sUID = $oUser->getUID();
|
||||
|
||||
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
|
||||
$sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase')
|
||||
?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
|
||||
$bAccountDefinedExplicitly = ($sEmail && $sPassword) && $sEmail === $oSettings->username;
|
||||
|
||||
$sNcEmail = $oUser->getEMailAddress() ?: $oUser->getPrimaryEMailAddress();
|
||||
|
||||
// Only login with OIDC access token if
|
||||
// it is enabled in config, the user is currently logged in with OIDC,
|
||||
// the current snappymail account is the OIDC account and no account defined explicitly
|
||||
if (\OC::$server->getConfig()->getAppValue('snappymail', 'snappymail-autologin-oidc', false)
|
||||
&& \OC::$server->getSession()->get('is_oidc')
|
||||
&& $sNcEmail === $oSettings->username
|
||||
&& !$bAccountDefinedExplicitly
|
||||
if ($oAccount instanceof \RainLoop\Model\MainAccount
|
||||
&& \OCA\SnappyMail\Util\SnappyMailHelper::isOIDCLogin()
|
||||
// && $oClient->supportsAuthType('OAUTHBEARER') // v2.28
|
||||
&& \str_starts_with($oSettings->passphrase, 'oidc_login|')
|
||||
) {
|
||||
$sAccessToken = \OC::$server->getSession()->get('oidc_access_token');
|
||||
if ($sAccessToken) {
|
||||
$oSettings->passphrase = $sAccessToken;
|
||||
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
|
||||
}
|
||||
// $oSettings->passphrase = \OC::$server->getSession()->get('snappymail-passphrase');
|
||||
$oSettings->passphrase = \OC::$server->getSession()->get('oidc_access_token');
|
||||
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,18 @@
|
|||
|
||||
// https://github.com/nextcloud/calendar/issues/4684
|
||||
if (cfg.CalDAV) {
|
||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICS" data-icon="📅">
|
||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSShow" data-icon="📅">
|
||||
<span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/SAVE_ICS"></span>
|
||||
</span>`));
|
||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSOldInvitation" data-icon="📅">
|
||||
<span data-i18n="NEXTCLOUD/OLD_INVITATION"></span>
|
||||
</span>`))
|
||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSNewInvitation" data-icon="📅">
|
||||
<span class="g-ui-link" data-bind="click: nextcloudSaveICS" data-i18n="NEXTCLOUD/UPDATE_ON_MY_CALENDAR"></span>
|
||||
</span>`))
|
||||
attachmentsControls.append(Element.fromHTML(`<span data-bind="visible: nextcloudICSLastInvitation" data-icon="📅">
|
||||
<span data-i18n="NEXTCLOUD/LAST_INVITATION"></span>
|
||||
</span>`))
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
@ -107,18 +116,169 @@
|
|||
};
|
||||
|
||||
view.nextcloudICS = ko.observable(null);
|
||||
view.nextcloudICSOldInvitation = ko.observable(null);
|
||||
view.nextcloudICSNewInvitation = ko.observable(null);
|
||||
view.nextcloudICSLastInvitation = ko.observable(null);
|
||||
|
||||
view.nextcloudSaveICS = () => {
|
||||
view.nextcloudICSShow = ko.observable(null);
|
||||
|
||||
view.nextcloudICSCalendar = ko.observable(null);
|
||||
view.filteredEventsUrls = ko.observable([]);
|
||||
|
||||
view.nextcloudSaveICS = async () => {
|
||||
let VEVENT = view.nextcloudICS();
|
||||
VEVENT && rl.nextcloud.selectCalendar()
|
||||
.then(href => href && rl.nextcloud.calendarPut(href, VEVENT));
|
||||
VEVENT = await view.handleUpdatedRecurrentEvents(VEVENT)
|
||||
VEVENT && rl.nextcloud.selectCalendar(VEVENT)
|
||||
}
|
||||
|
||||
|
||||
view.handleUpdatedRecurrentEvents = async (VEVENT) => {
|
||||
|
||||
const uid = VEVENT.UID
|
||||
|
||||
let makePUTRequest = false
|
||||
|
||||
const filteredEventUrls = view.filteredEventUrls
|
||||
|
||||
if (filteredEventUrls.length > 1) {
|
||||
// console.warn('filteredEventUrls.length > 1')
|
||||
}
|
||||
else if (filteredEventUrls.length == 1) {
|
||||
const eventUrl = filteredEventUrls[0]
|
||||
const eventText = await fetchEvent(eventUrl)
|
||||
|
||||
// don't do anything for equal cards
|
||||
if (VEVENT.rawText == eventText) {
|
||||
return VEVENT
|
||||
}
|
||||
|
||||
const newVeventsText = extractVEVENTs(VEVENT.rawText)
|
||||
const oldVeventsText = extractVEVENTs(eventText)
|
||||
|
||||
// if there's only one event in the old card, it can't be an edit of the exception card
|
||||
if (oldVeventsText.length == 1) {
|
||||
const newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventsText[0])
|
||||
|
||||
// if there's a property RECURRENCE-ID in the new card, it's an recurrence exception event
|
||||
if (newRecurrenceId.length > 0) {
|
||||
const updatedVEVENT = {
|
||||
'SUMMARY' : VEVENT.SUMMARY,
|
||||
'UID' : VEVENT.UID,
|
||||
'rawText' : mergeEventTexts(eventText, newVeventsText[0])
|
||||
}
|
||||
VEVENT = updatedVEVENT
|
||||
makePUTRequest = true
|
||||
}
|
||||
else {
|
||||
return VEVENT
|
||||
}
|
||||
}
|
||||
// if there's more than one event in the old card, it's possible to be an inclusion of a new exception card
|
||||
// or update of old exception card
|
||||
else {
|
||||
let recurrenceIdMatch = false
|
||||
|
||||
let isUpdate = false
|
||||
const updateData = {
|
||||
'oldEventIndex': null,
|
||||
'newEventIndex': null,
|
||||
}
|
||||
|
||||
// check if it's an update
|
||||
for (let i = 0; i < oldVeventsText.length; i++) {
|
||||
let oldVeventText = oldVeventsText[i]
|
||||
|
||||
for (let j = 0; j < newVeventsText.length; j++) {
|
||||
let newVeventText = newVeventsText[j]
|
||||
|
||||
let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText)
|
||||
let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText)
|
||||
let oldSequence = extractProperties('SEQUENCE', oldVeventText)
|
||||
let newSequence = extractProperties('SEQUENCE', newVeventText)
|
||||
|
||||
if (oldRecurrenceId.length == 0 || newRecurrenceId.length == 0 || oldSequence.length == 0 || newSequence.length == 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (oldRecurrenceId[0] == newRecurrenceId[0]) {
|
||||
if (newSequence[0] > oldSequence[0]) {
|
||||
isUpdate = true
|
||||
|
||||
updateData.oldEventIndex = i
|
||||
updateData.newEventIndex = j
|
||||
|
||||
i = oldVeventsText.length
|
||||
j = newVeventsText.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if it's an update...
|
||||
if (isUpdate) {
|
||||
// substitute old event text for new event text
|
||||
const oldEventStart = eventText.indexOf(oldVeventsText[updateData.oldEventIndex])
|
||||
const oldEventEnd = oldEventStart + oldVeventsText[updateData.oldEventIndex].length
|
||||
|
||||
const newEvent = eventText.substring(0, oldEventStart) + newVeventsText[updateData.newEventIndex] + eventText.substring(oldEventEnd)
|
||||
|
||||
const updatedVEVENT = {
|
||||
'SUMMARY' : VEVENT.SUMMARY,
|
||||
'UID': VEVENT.UID,
|
||||
'rawText' : newEvent
|
||||
}
|
||||
VEVENT = updatedVEVENT
|
||||
makePUTRequest = true
|
||||
}
|
||||
// if it's not an update, it's an inclusion, as there's no match of RECURRENCE-ID
|
||||
else {
|
||||
const updatedVEVENT = {
|
||||
'SUMMARY' : VEVENT.SUMMARY,
|
||||
'UID' : VEVENT.UID,
|
||||
'rawText' : mergeEventTexts(eventText, newVeventsText[0])
|
||||
}
|
||||
VEVENT = updatedVEVENT
|
||||
makePUTRequest = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (makePUTRequest) {
|
||||
let href = "/" + (filteredEventUrls[0].split('/').slice(5, -1)[0])
|
||||
|
||||
rl.nextcloud.calendarPut(href, VEVENT, (response) => {
|
||||
if (response.status != 201 && response.status != 204) {
|
||||
InvitesPopupView.showModal([
|
||||
rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_TITLE'),
|
||||
rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_BODY', {eventName: VEVENT.SUMMARY})
|
||||
])
|
||||
return
|
||||
}
|
||||
InvitesPopupView.showModal([
|
||||
rl.i18n('NEXTCLOUD/EVENT_UPDATED_TITLE'),
|
||||
rl.i18n('NEXTCLOUD/EVENT_UPDATED_BODY', {eventName: VEVENT.SUMMARY})
|
||||
])
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return VEVENT
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
view.message.subscribe(msg => {
|
||||
view.nextcloudICS(null);
|
||||
view.nextcloudICSOldInvitation(null);
|
||||
view.nextcloudICSNewInvitation(null);
|
||||
view.nextcloudICSLastInvitation(null);
|
||||
view.nextcloudICSShow(view.nextcloudICS())
|
||||
|
||||
|
||||
if (msg && cfg.CalDAV) {
|
||||
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
|
||||
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
|
||||
|
@ -126,7 +286,7 @@
|
|||
// fetch it and parse the VEVENT
|
||||
rl.fetch(ics.linkDownload())
|
||||
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||
.then(text => {
|
||||
.then(async (text) => {
|
||||
let VEVENT,
|
||||
VALARM,
|
||||
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
||||
|
@ -177,6 +337,158 @@
|
|||
shouldReply: VEVENT.shouldReply()
|
||||
});
|
||||
view.nextcloudICS(VEVENT);
|
||||
view.nextcloudICSShow(true);
|
||||
|
||||
|
||||
// try to get calendars, save
|
||||
const calendarUrls = await fetchCalendarUrls()
|
||||
|
||||
const filteredEventUrls = []
|
||||
for (let i = 0; i < calendarUrls.length; i++) {
|
||||
let calendarUrl = calendarUrls[i]
|
||||
|
||||
const skipCalendars = ['/inbox/', '/outbox/', '/trashbin/']
|
||||
let skip = false
|
||||
for (let j = 0; j < skipCalendars.length; j++) {
|
||||
if (calendarUrl.includes(skipCalendars[j])) {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (skip) {
|
||||
continue
|
||||
}
|
||||
|
||||
// try to get event
|
||||
const eventUrls = await fetchEventUrl(calendarUrl, VEVENT.UID)
|
||||
|
||||
if (eventUrls.length == 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
eventUrls.forEach((url) => {
|
||||
filteredEventUrls.push(url)
|
||||
})
|
||||
}
|
||||
view.filteredEventUrls = filteredEventUrls
|
||||
|
||||
// if there's none, save in view.nextcloudICS
|
||||
if (filteredEventUrls.length == 0) {
|
||||
view.nextcloudICS(VEVENT);
|
||||
view.nextcloudICSShow(true)
|
||||
}
|
||||
// if there's some...
|
||||
else {
|
||||
const savedEvent = await fetchEvent(filteredEventUrls[0])
|
||||
|
||||
const newVeventsText = extractVEVENTs(VEVENT.rawText)
|
||||
const oldVeventsText = extractVEVENTs(savedEvent)
|
||||
|
||||
// if there's more than one event in the old card, it's possible to be inclusion of new exception card
|
||||
// or updated of old exception card
|
||||
if (oldVeventsText.length > 1) {
|
||||
let recurrenceIdMatch = false
|
||||
|
||||
let oldNewestCreated = null
|
||||
let newNewestCreated = null
|
||||
|
||||
// check if it's an update
|
||||
for (let i = 0; i < oldVeventsText.length; i++) {
|
||||
let oldVeventText = oldVeventsText[i]
|
||||
|
||||
for (let j = 0; j < newVeventsText.length; j++) {
|
||||
let newVeventText = newVeventsText[j]
|
||||
|
||||
let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText)
|
||||
let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText)
|
||||
let oldSequence = extractProperties('SEQUENCE', oldVeventText)
|
||||
let newSequence = extractProperties('SEQUENCE', newVeventText)
|
||||
|
||||
if (newRecurrenceId.length == 0 && oldRecurrenceId.length == 1) {
|
||||
view.nextcloudICSShow(false)
|
||||
view.nextcloudICSOldInvitation(true)
|
||||
}
|
||||
|
||||
if (oldRecurrenceId.length > 0 && newRecurrenceId.length > 0 && oldSequence.length > 0 && newSequence.length > 0) {
|
||||
if (oldRecurrenceId[0] == newRecurrenceId[0]) {
|
||||
if (newSequence[0] < oldSequence[0]) {
|
||||
view.nextcloudICSOldInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
else if (newSequence[0] == oldSequence[0]) {
|
||||
view.nextcloudICSLastInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
else if (newSequence[0] > oldSequence[0]) {
|
||||
view.nextcloudICSNewInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
|
||||
// exit for loops
|
||||
j = newVeventsText.length
|
||||
i = oldVeventsText.length
|
||||
}
|
||||
}
|
||||
|
||||
let oldCreated = extractProperties('CREATED', oldVeventText)
|
||||
let newCreated = extractProperties('CREATED', newVeventText)
|
||||
|
||||
if (oldCreated.length == 0 || newCreated.length == 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const formattedOldDate = oldCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||
const formattedNewDate = newCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||
|
||||
const oldDate = new Date(formattedOldDate)
|
||||
const newDate = new Date(formattedNewDate)
|
||||
|
||||
if (oldNewestCreated == null || oldDate > oldNewestCreated) {
|
||||
oldNewestCreated = oldDate
|
||||
}
|
||||
if (newNewestCreated == null || newDate > newNewestCreated) {
|
||||
newNewestCreated = newDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newNewestCreated != null && oldNewestCreated != null) {
|
||||
if (newNewestCreated < oldNewestCreated) {
|
||||
view.nextcloudICSOldInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
else if (newNewestCreated == oldNewestCreated) {
|
||||
view.nextcloudICSLastInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const oldLastModified = extractProperties('LAST-MODIFIED', oldVeventsText[0])
|
||||
const newLastModified = extractProperties('LAST-MODIFIED', newVeventsText[0])
|
||||
|
||||
if (oldLastModified.length == 1 && newLastModified.length == 1) {
|
||||
const formattedOldDate = oldLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||
const formattedNewDate = newLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z');
|
||||
|
||||
const oldDate = new Date(formattedOldDate)
|
||||
const newDate = new Date(formattedNewDate)
|
||||
|
||||
if (newDate > oldDate) {
|
||||
view.nextcloudICSNewInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
else if (newDate < oldDate) {
|
||||
view.nextcloudICSOldInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
else {
|
||||
view.nextcloudICSLastInvitation(true)
|
||||
view.nextcloudICSShow(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -186,3 +498,148 @@
|
|||
});
|
||||
|
||||
})(window.rl);
|
||||
|
||||
|
||||
async function fetchCalendarUrls () {
|
||||
const username = OC.currentUser
|
||||
const requestToken = OC.requestToken
|
||||
|
||||
const url = '/remote.php/dav/calendars/' + username
|
||||
const response = await fetch(url, {
|
||||
'method': 'PROPFIND',
|
||||
'headers': {
|
||||
'Depth': '1',
|
||||
'Content-Type': 'application/xml',
|
||||
'requesttoken' : requestToken
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Error fetching calendars', response)
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(responseText, 'application/xml')
|
||||
|
||||
const calendarUrls = []
|
||||
|
||||
const hrefElements = xmlDoc.getElementsByTagName('d:href')
|
||||
for (let i = 1; i < hrefElements.length; i++) {
|
||||
let calendarUrl = hrefElements[i].textContent.trim()
|
||||
calendarUrls.push(calendarUrl)
|
||||
}
|
||||
|
||||
return calendarUrls
|
||||
}
|
||||
|
||||
|
||||
async function fetchEventUrl (calendarUrl, uid) {
|
||||
const requestToken = OC.requestToken
|
||||
|
||||
const xmlRequestBody = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
|
||||
<C:prop>
|
||||
<C:calendar-data/>
|
||||
</C:prop>
|
||||
<C:filter>
|
||||
<C:comp-filter name="VCALENDAR">
|
||||
<C:comp-filter name="VEVENT">
|
||||
<C:prop-filter name="UID">
|
||||
<C:text-match collation="i;unicode-casemap" match-type="equals">${uid}</C:text-match>
|
||||
</C:prop-filter>
|
||||
</C:comp-filter>
|
||||
</C:comp-filter>
|
||||
</C:filter>
|
||||
</C:calendar-query>`
|
||||
|
||||
const response = await fetch(calendarUrl, {
|
||||
'method': 'REPORT',
|
||||
'headers': {
|
||||
'Content-Type': 'application/xml',
|
||||
'Depth': 1,
|
||||
'requesttoken': requestToken
|
||||
},
|
||||
'body': xmlRequestBody
|
||||
})
|
||||
|
||||
const responseText = await response.text()
|
||||
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(responseText, 'application/xml')
|
||||
|
||||
const hrefElements = xmlDoc.getElementsByTagName('d:href')
|
||||
const hrefValues = Array.from(hrefElements).map(element => element.textContent)
|
||||
|
||||
return hrefValues
|
||||
}
|
||||
|
||||
|
||||
async function fetchEvent (eventUrl) {
|
||||
const requestToken = OC.requestToken
|
||||
const response = await fetch(eventUrl, {
|
||||
'method': 'GET',
|
||||
'headers': {
|
||||
'requesttoken': requestToken
|
||||
}
|
||||
})
|
||||
const responseText = await response.text()
|
||||
|
||||
return responseText
|
||||
}
|
||||
|
||||
|
||||
function extractVEVENTs(text) {
|
||||
let lastEndIndex = 0
|
||||
const vEvents = []
|
||||
|
||||
let textToSearch = ""
|
||||
let beginIndex = 0
|
||||
let endIndex = 0
|
||||
let foundVevent = false
|
||||
|
||||
while (true) {
|
||||
textToSearch = text.substring(lastEndIndex)
|
||||
|
||||
beginIndex = textToSearch.indexOf('BEGIN:VEVENT')
|
||||
if (beginIndex == -1) {
|
||||
break
|
||||
}
|
||||
endIndex = textToSearch.substring(beginIndex).indexOf('END:VEVENT')
|
||||
if (endIndex == -1) {
|
||||
break
|
||||
}
|
||||
endIndex += beginIndex
|
||||
|
||||
lastEndIndex = lastEndIndex + endIndex + 'END:VEVENT'.length
|
||||
|
||||
foundVevent = textToSearch.substring(beginIndex + 'BEGIN:VEVENT'.length, endIndex)
|
||||
|
||||
vEvents.push(foundVevent)
|
||||
}
|
||||
|
||||
return vEvents
|
||||
}
|
||||
|
||||
|
||||
function extractProperties (property, text) {
|
||||
const matches = text.match(`${property}.*`)
|
||||
|
||||
if (matches == null) {
|
||||
return []
|
||||
}
|
||||
|
||||
const separatedMatches = matches.map((match) => {
|
||||
return match.substring(property.length + 1)
|
||||
})
|
||||
|
||||
return separatedMatches
|
||||
}
|
||||
|
||||
|
||||
function mergeEventTexts (oldEventText, newEventText) {
|
||||
const appendIndex = oldEventText.indexOf('END:VEVENT') + 'END:VEVENT'.length
|
||||
const updatedEventText = oldEventText.substring(0, appendIndex) + "\nBEGIN:VEVENT" + newEventText + "END:VEVENT" + oldEventText.substring(appendIndex)
|
||||
return updatedEventText
|
||||
}
|
||||
|
|
|
@ -320,12 +320,28 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
|
|||
}
|
||||
|
||||
onBuild(dom) {
|
||||
let modalObj = this
|
||||
this.tree = dom.querySelector('#sm-nc-calendars');
|
||||
this.tree.addEventListener('click', event => {
|
||||
let el = event.target;
|
||||
if (el.matches('button')) {
|
||||
this.select = el.href;
|
||||
this.close();
|
||||
let VEVENT = this.VEVENT
|
||||
|
||||
rl.nextcloud.calendarPut(this.select, this.VEVENT, (response) => {
|
||||
if (response.status != 201 && response.status != 204) {
|
||||
InvitesPopupView.showModal([
|
||||
rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_TITLE'),
|
||||
rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_BODY', {eventName: VEVENT.SUMMARY}),
|
||||
() => {modalObj.close()}
|
||||
])
|
||||
}
|
||||
InvitesPopupView.showModal([
|
||||
rl.i18n('MESSAGE/EVENT_ADDED_TITLE'),
|
||||
rl.i18n('MESSAGE/EVENT_ADDED_BODY', {eventName: VEVENT.SUMMARY}),
|
||||
() => {modalObj.close()}
|
||||
])
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -375,9 +391,10 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView {
|
|||
treeElement.appendChild(li);
|
||||
}
|
||||
// Happens after showModal()
|
||||
beforeShow(fResolve) {
|
||||
beforeShow(fResolve, VEVENT) {
|
||||
this.select = '';
|
||||
this.fResolve = fResolve;
|
||||
this.VEVENT = VEVENT
|
||||
this.tree.innerHTML = '';
|
||||
davFetch('calendars', '/', {
|
||||
method: 'PROPFIND',
|
||||
|
@ -432,14 +449,15 @@ close() {}
|
|||
}
|
||||
|
||||
rl.nextcloud = {
|
||||
selectCalendar: () =>
|
||||
selectCalendar: (VEVENT) =>
|
||||
new Promise(resolve => {
|
||||
NextcloudCalendarsPopupView.showModal([
|
||||
href => resolve(href),
|
||||
VEVENT
|
||||
]);
|
||||
}),
|
||||
|
||||
calendarPut: (path, event) => {
|
||||
calendarPut: (path, event, callback) => {
|
||||
davFetch('calendars', path + '/' + event.UID + '.ics', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
@ -463,6 +481,8 @@ rl.nextcloud = {
|
|||
// response.text().then(text => console.error({status:response.status, body:text}));
|
||||
Promise.reject(new Error({ response }));
|
||||
}
|
||||
|
||||
callback && callback(response)
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -500,3 +520,27 @@ function getElementsInNamespaces(xmlDocument, tagName) {
|
|||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
class InvitesPopupView extends rl.pluginPopupView {
|
||||
constructor() {
|
||||
super('NextcloudInvites')
|
||||
}
|
||||
|
||||
onBuild(dom) {
|
||||
this.title = dom.querySelector('#sm-invites-popup-title')
|
||||
this.body = dom.querySelector('#sm-invites-popup-body')
|
||||
}
|
||||
|
||||
beforeShow(title, body, onHide_) {
|
||||
this.title.innerHTML = title
|
||||
this.body.innerHTML = body
|
||||
this.onHide_ = onHide_
|
||||
}
|
||||
|
||||
onHide() {
|
||||
if (this.onHide_) {
|
||||
this.onHide_()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,21 @@
|
|||
"NEXTCLOUD": {
|
||||
"SAVE_ATTACHMENTS": "Save in Nextcloud",
|
||||
"SAVE_EML": "Save as .eml in Nextcloud",
|
||||
"SAVE_ICS": "Add to calendar",
|
||||
"SELECT_FOLDER": "Select folder",
|
||||
"SELECT_FILES": "Select file(s)",
|
||||
"ATTACH_FILES": "Attach Nextcloud files",
|
||||
"SELECT_CALENDAR": "Select calendar",
|
||||
"FILE_ATTACH": "attach",
|
||||
"FILE_INTERNAL": "internal",
|
||||
"FILE_PUBLIC": "public"
|
||||
"FILE_PUBLIC": "public",
|
||||
|
||||
"SELECT_CALENDAR": "Select calendar",
|
||||
"SAVE_ICS": "Add to calendar",
|
||||
"OLD_INVITATION": "Old invitation",
|
||||
"UPDATE_ON_MY_CALENDAR": "Update invitation",
|
||||
"LAST_INVITATION": "Last invitation",
|
||||
"EVENT_UPDATE_FAILURE_TITLE": "Update failed",
|
||||
"EVENT_UPDATE_FAILURE_BODY": "Why, i have no clue",
|
||||
"EVENT_UPDATED_TITLE": "Updated",
|
||||
"EVENT_UPDATED_BODY": "Why, i have no clue"
|
||||
}
|
||||
}
|
||||
|
|
8
plugins/nextcloud/templates/PopupsNextcloudInvites.html
Normal file
8
plugins/nextcloud/templates/PopupsNextcloudInvites.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<header>
|
||||
<a class="close" href="#" data-bind="click: close">×</a>
|
||||
<h3 id="sm-invites-popup-title"></h3>
|
||||
</header>
|
||||
<div class="modal-body form-horizontal" id="sm-invites-popup-body">
|
||||
</div>
|
||||
<footer>
|
||||
</footer>
|
5
plugins/view-ics/ical.es5.min.cjs
Normal file
5
plugins/view-ics/ical.es5.min.cjs
Normal file
File diff suppressed because one or more lines are too long
|
@ -14,6 +14,7 @@ class ViewICSPlugin extends \RainLoop\Plugins\AbstractPlugin
|
|||
{
|
||||
// $this->UseLangs(true);
|
||||
$this->addJs('message.js');
|
||||
$this->addJs('ical.es5.min.cjs');
|
||||
$this->addJs('windowsZones.js');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,13 +36,13 @@
|
|||
<details data-bind="if: viewICS, visible: viewICS">
|
||||
<summary data-icon="📅" data-bind="text: viewICS().SUMMARY"></summary>
|
||||
<table><tbody style="white-space:pre">
|
||||
<tr data-bind="visible: viewICS().ORGANIZER"><td>Organizer</td><td data-bind="text: viewICS().ORGANIZER"></td></tr>
|
||||
<tr><td>Start</td><td data-bind="text: viewICS().DTSTART"></td></tr>
|
||||
<tr><td>End</td><td data-bind="text: viewICS().DTEND"></td></tr>
|
||||
<tr data-bind="visible: viewICS().ORGANIZER_TXT"><td>Organizer: </td><td><a data-bind="text: viewICS().ORGANIZER_TXT, attr: { href: viewICS().ORGANIZER_MAIL }"></a></td></tr>
|
||||
<tr><td>Start: </td><td data-bind="text: viewICS().DTSTART"></td></tr>
|
||||
<tr><td>End: </td><td data-bind="text: viewICS().DTEND"></td></tr>
|
||||
<tr data-bind="visible: viewICS().LOCATION"><td>Location: </td><td data-bind="text: viewICS().LOCATION"></td></tr>
|
||||
<!-- <tr><td>Transparency</td><td data-bind="text: viewICS().TRANSP"></td></tr>-->
|
||||
<tr data-bind="foreach: viewICS().ATTENDEE">
|
||||
<td></td><td data-bind="text: $data.replace(/;/g,';\\n')"></td>
|
||||
</tr>
|
||||
<tr><td>Attendees: </td><td data-bind="foreach: viewICS().ATTENDEE"><span data-bind="text: $data.replace(/;/g,';\\n')"></span> </td>
|
||||
|
||||
</tbody></table>
|
||||
</details>`));
|
||||
|
||||
|
@ -88,55 +88,33 @@
|
|||
});
|
||||
// ICS attachment
|
||||
// let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
|
||||
|
||||
let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
|
||||
if (ics && ics.download) {
|
||||
|
||||
// fetch it and parse the VEVENT
|
||||
rl.fetch(ics.linkDownload())
|
||||
.then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response })))
|
||||
.then(text => {
|
||||
let VEVENT,
|
||||
VALARM,
|
||||
multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE',
|
||||
'EXRULE','RSTATUS','RELATED','RESOURCES','RDATE','RRULE'],
|
||||
lines = text.split(/\r?\n/),
|
||||
i = lines.length;
|
||||
while (i--) {
|
||||
let line = lines[i];
|
||||
if (VEVENT) {
|
||||
while (line.startsWith(' ') && i--) {
|
||||
line = lines[i] + line.slice(1);
|
||||
}
|
||||
if (line.startsWith('END:VALARM')) {
|
||||
VALARM = {};
|
||||
continue;
|
||||
} else if (line.startsWith('BEGIN:VALARM')) {
|
||||
VEVENT.VALARM || (VEVENT.VALARM = []);
|
||||
VEVENT.VALARM.push(VALARM);
|
||||
VALARM = null;
|
||||
continue;
|
||||
} else if (line.startsWith('BEGIN:VEVENT')) {
|
||||
break;
|
||||
}
|
||||
line = line.match(/^([^:;]+)[:;](.+)$/);
|
||||
if (line) {
|
||||
if (VALARM) {
|
||||
VALARM[line[1]] = line[2];
|
||||
} else if (multiple.includes(line[1]) || 'X-' == line[1].slice(0,2)) {
|
||||
VEVENT[line[1]] || (VEVENT[line[1]] = []);
|
||||
VEVENT[line[1]].push(line[2]);
|
||||
} else {
|
||||
if ('DTSTART' === line[1] || 'DTEND' === line[1]) {
|
||||
line[2] = parseDate(line[2]);
|
||||
}
|
||||
VEVENT[line[1]] = line[2];
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('END:VEVENT')) {
|
||||
VEVENT = {};
|
||||
}
|
||||
let jcalData = ICAL.parse(text)
|
||||
var comp = new ICAL.Component(jcalData);
|
||||
var vevent = comp.getFirstSubcomponent("vevent");
|
||||
var event = new ICAL.Event(vevent);
|
||||
let VEVENT = {};
|
||||
if(event.organizer.startsWith("mailto:")){
|
||||
VEVENT.ORGANIZER_TXT=event.organizer.substr(7)
|
||||
VEVENT.ORGANIZER_MAIL = event.organizer
|
||||
} else
|
||||
VEVENT.ORGANIZER_TXT=event.organizer
|
||||
VEVENT.SUMMARY = event.summary;
|
||||
VEVENT.DTSTART = parseDate(vevent.getFirstPropertyValue("dtstart"));
|
||||
VEVENT.DTEND = parseDate(vevent.getFirstPropertyValue("dtend"));
|
||||
VEVENT.LOCATION = event.location;
|
||||
VEVENT.ATTENDEE = []
|
||||
for(let attendee of event.attendees){
|
||||
VEVENT.ATTENDEE.push(attendee.getFirstParameter("cn"));
|
||||
}
|
||||
// METHOD:REPLY || METHOD:REQUEST
|
||||
// console.dir({VEVENT:VEVENT});
|
||||
|
||||
if (VEVENT) {
|
||||
VEVENT.rawText = text;
|
||||
VEVENT.isCancelled = () => VEVENT.STATUS?.includes('CANCELLED');
|
||||
|
|
|
@ -749,9 +749,14 @@ class MailClient
|
|||
*/
|
||||
public function MessageList(MessageListParams $oParams) : MessageCollection
|
||||
{
|
||||
if (0 > $oParams->iOffset || 0 > $oParams->iLimit || 999 < $oParams->iLimit) {
|
||||
if (0 > $oParams->iOffset || 0 > $oParams->iLimit) {
|
||||
throw new \ValueError;
|
||||
}
|
||||
if (10 > $oParams->iLimit) {
|
||||
$oParams->iLimit = 10;
|
||||
} else if (999 < $oParams->iLimit) {
|
||||
$oParams->iLimit = 50;
|
||||
}
|
||||
|
||||
$sSearch = \trim($oParams->sSearch);
|
||||
|
||||
|
|
|
@ -330,7 +330,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bodyText"></div>
|
||||
<div class="bodyText" dir="auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
6
vendors/bootstrap/less/forms.less
vendored
6
vendors/bootstrap/less/forms.less
vendored
|
@ -62,6 +62,12 @@ input {
|
|||
}
|
||||
}
|
||||
|
||||
//input:invalid,
|
||||
input:user-invalid {
|
||||
background-color: var(--error-bg-clr, #f2dede);
|
||||
border-color: var(--error-border-clr, #eed3d7);
|
||||
color: var(--error-clr, #b94a48);
|
||||
}
|
||||
|
||||
// Position radios and checkboxes better
|
||||
input[type="radio"],
|
||||
|
|
|
@ -51,6 +51,7 @@ ko.utils = {
|
|||
: node => node.cloneNode(true)),
|
||||
|
||||
setDomNodeChildren: (domNode, childNodes) => {
|
||||
// domNode.replaceChildren(...childNodes);
|
||||
ko.utils.emptyDomNode(domNode);
|
||||
childNodes && domNode.append(...childNodes);
|
||||
},
|
||||
|
@ -2821,7 +2822,7 @@ ko.bindingHandlers['textInput'] = {
|
|||
elementValueBeforeEvent = timeoutHandle = undefined;
|
||||
|
||||
var elementValue = element.value;
|
||||
if (previousElementValue !== elementValue) {
|
||||
if (element.checkValidity() && previousElementValue !== elementValue) {
|
||||
// Provide a way for tests to know exactly which event was processed
|
||||
previousElementValue = elementValue;
|
||||
ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue);
|
||||
|
|
|
@ -53,8 +53,8 @@ var ba={};c.i.options={init:a=>{if(!a.matches("SELECT"))throw Error("options bin
|
|||
e?f=b().map(c.C.M):0<=a.selectedIndex&&f.push(c.C.M(a.options[a.selectedIndex]));if(l){Array.isArray(l)||(l=[l]);var n=l.filter(m=>m??1)}var p=!1;l=k;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&(l=(m,q)=>{k(m,q);c.u.I(d.get("optionsAfterRender"),null,[q[0],m!==ba?m:void 0])});c.g.Ab(a,n,(m,q,r)=>{r.length&&(f=r[0].selected?[c.C.M(r[0])]:[],p=!0);q=a.ownerDocument.createElement("option");m===ba?(c.g.Za(q),c.C.Fa(q,void 0)):(r=h(m,d.get("optionsValue"),m),c.C.Fa(q,c.g.h(r)),
|
||||
m=h(m,d.get("optionsText"),r),c.g.Za(q,m));return[q]},{},l);n=f.length;(e?n&&b().length<n:n&&0<=a.selectedIndex?c.C.M(a.options[a.selectedIndex])!==f[0]:n||0<=a.selectedIndex)&&c.u.I(c.g.Db,null,[a,"change"]);c.u.Ca()&&c.j.notify(a,c.j.F);g&&20<Math.abs(g-a.scrollTop)&&(a.scrollTop=g)}};c.i.options.Wa=c.g.l.Z();c.i.style={update:(a,b)=>{c.g.K(c.g.h(b()||{}),(d,e)=>{e=c.g.h(e);if(null==e||!1===e)e="";if(/^--/.test(d))a.style.setProperty(d,e);else{d=d.replace(/-(\w)/g,(l,f)=>f.toUpperCase());var g=
|
||||
a.style[d];a.style[d]=e;e===g||a.style[d]!=g||isNaN(e)||(a.style[d]=e+"px")}})}};c.i.submit={init:(a,b,d,e,g)=>{if("function"!=typeof b())throw Error("The value for a submit binding must be a function");a.addEventListener("submit",l=>{var f=b();try{var h=f.call(g.$data,a)}finally{!0!==h&&l.preventDefault()}})}};c.i.text={init:()=>({controlsDescendantBindings:!0}),update:(a,b)=>{8===a.nodeType&&(a.text||a.after(a.text=J.createTextNode("")),a=a.text);c.g.Za(a,b())}};c.m.aa.text=!0;c.i.textInput={init:(a,
|
||||
b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;e!==k&&(e=k,c.la.Ga(b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var m=b(),q=c.C.M(a);c.la.Ga(m,d,"value",
|
||||
q)};f&&("string"==typeof f?l.add(f):f.forEach(m=>l.add(m)),l.delete("change"));l.forEach(m=>{var q=k;(m||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},m=m.slice(5));a.addEventListener(m,q)});var n=g&&"file"==a.type?()=>{var m=c.g.h(b());null==m||""===m?a.value="":c.u.I(k)}:()=>{var m=c.g.h(b()),q=c.C.M(a);if(null!==h&&m===h)setTimeout(n,0);else if(m!==q||void 0===q)e?(c.C.Fa(a,m),m!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,m)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")?
|
||||
b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;a.checkValidity()&&e!==k&&(e=k,c.la.Ga(b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var m=b(),q=c.C.M(a);
|
||||
c.la.Ga(m,d,"value",q)};f&&("string"==typeof f?l.add(f):f.forEach(m=>l.add(m)),l.delete("change"));l.forEach(m=>{var q=k;(m||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},m=m.slice(5));a.addEventListener(m,q)});var n=g&&"file"==a.type?()=>{var m=c.g.h(b());null==m||""===m?a.value="":c.u.I(k)}:()=>{var m=c.g.h(b()),q=c.C.M(a);if(null!==h&&m===h)setTimeout(n,0);else if(m!==q||void 0===q)e?(c.C.Fa(a,m),m!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,m)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")?
|
||||
n():k():(a.addEventListener("change",k),p=c.o(n,{s:a}))},null,{notifyImmediately:!0})}else a.addEventListener("change",k),c.o(n,{s:a})}else c.applyBindingAccessorsToNode(a,{checkedValue:b})},update:()=>{}};c.i.visible={update:(a,b)=>{b=c.g.h(b());var d="none"!=a.style.display;b&&!d?a.style.display="":d&&!b&&(a.style.display="none")}};c.i.hidden={update:(a,b)=>a.hidden=!!c.g.h(b())};(function(a){c.i[a]={init:function(b,d,e,g,l){return c.i.event.init.call(this,b,()=>({[a]:d()}),e,g,l)}}})("click");
|
||||
(()=>{let a=c.g.l.Z();class b{constructor(e){this.Na=e}Ua(...e){let g=this.Na;if(!e.length)return c.g.l.get(g,a)||(11===this.H?g.content:1===this.H?g:void 0);c.g.l.set(g,a,e[0])}}class d extends b{constructor(e){super(e);e&&(this.H=e.matches("TEMPLATE")&&e.content?e.content.nodeType:1)}}c.bb={Na:d,lb:b}})();(()=>{const a=(h,k,n)=>{var p;for(k=c.m.nextSibling(k);h&&(p=h)!==k;)h=c.m.nextSibling(p),n(p,h)},b=(h,k)=>{if(h.length){var n=h[0],p=n.parentNode;a(n,h[h.length-1],m=>(1===m.nodeType||8===m.nodeType)&&
|
||||
c.Ib(k,m));c.g.xa(h,p)}},d=(h,k,n,p)=>{var m=(h&&(h.nodeType?h:0<h.length?h[0]:null)||n||{}).ownerDocument;if("string"==typeof n){m=m||J;m=m.getElementById(n);if(!m)throw Error("Cannot find template with ID "+n);n=new c.bb.Na(m)}else if([1,8].includes(n.nodeType))n=new c.bb.lb(n);else throw Error("Unknown template type: "+n);n=(n=n.Ua?n.Ua():null)?[...n.cloneNode(!0).childNodes]:null;if(!Array.isArray(n)||0<n.length&&"number"!=typeof n[0].nodeType)throw Error("Template engine must return an array of DOM nodes");
|
||||
|
|
|
@ -10,7 +10,7 @@ ko.bindingHandlers['textInput'] = {
|
|||
elementValueBeforeEvent = timeoutHandle = undefined;
|
||||
|
||||
var elementValue = element.value;
|
||||
if (previousElementValue !== elementValue) {
|
||||
if (element.checkValidity() && previousElementValue !== elementValue) {
|
||||
// Provide a way for tests to know exactly which event was processed
|
||||
previousElementValue = elementValue;
|
||||
ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue);
|
||||
|
|
3183
vendors/squire/build/squire-raw.js
vendored
3183
vendors/squire/build/squire-raw.js
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue