Files
sogo/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox
T
Jan Kahmen 0d66d7503d fix(mail): stop interpreting iMIP card text fields as markup
the whole server built html of an iMIP card is rendered through sg-compile
(Message.service.js sets part.compile for UIxMailPartICalViewer), and the user
comment and the description are emitted with escapeHTML="NO". event data from
the invitation is therefore both an angular template and raw html.

47133fdf3 closed the description against interpolation with ng-non-bindable.
this closes both paths for every remaining field:

* ng-non-bindable on the user comment, the location, the organizer common name
  and the raw body shown when the calendar cannot be parsed
* the user comment and the description are read through accessors that escape
  them, so the url detection and insertBR produce the only markup left in the
  output

the marker sits on the leaf containers, not on md-card-content, so the buttons,
the delegation autocomplete and the attendee chips keep being compiled. url
detection still turns a bare url in a comment or a description into a link.
2026-08-17 23:03:41 +02:00

315 lines
14 KiB
XML

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE div>
<div xmlns="http://www.w3.org/1999/xhtml"
xmlns:var="http://www.skyrix.com/od/binding"
xmlns:label="OGo:label"
xmlns:const="http://www.skyrix.com/od/constant"
xmlns:rsrc="OGo:url">
<!-- PARSING ERROR -->
<var:if condition="couldParseCalendar" const:negate="1">
<div>
<md-toolbar class="md-medium-tall md-warn md-hue-3">
<div class="md-toolbar-tools">
<h2>
<var:string label:value="SOGo could not parse the body of this calendar MIME part."/>
</h2>
</div>
</md-toolbar>
<md-whiteframe class="md-whiteframe-z1" layout="row" layout-align="space-between center">
<pre ng-non-bindable=""><var:string value="flatContentAsString" /></pre>
</md-whiteframe>
</div>
</var:if> <!-- if condition="couldParseCalendar" const:negate="1" -->
<!-- EVENT PROPERLY PARSED -->
<var:if condition="couldParseCalendar">
<md-card sg-imip="message" var:sg-imip-path="pathToAttachmentFromMessage">
<!-- TODO - remove unused strings
<legend>
<var:string label:value="Appointment"/>:
<strong><var:string value="inEvent.summary" /></strong>
<var:if condition="isLoggedInUserTheOrganizer">
(<var:string label:value="organized_by_you"/>)
</var:if>
<var:if condition="isLoggedInUserAnAttendee">
(<var:string label:value="you_are_an_attendee"/>)
</var:if>
</legend> -->
<!-- IMIP REQUEST -->
<md-card-content>
<var:if condition="inCalendar.method.uppercaseString" const:value="REQUEST">
<!-- sent to attendees to propose or update a meeting -->
<var:if condition="isLoggedInUserAnAttendee">
<var:if condition="hasCalendarAccess">
<div layout="row" layout-align="start center">
<var:if condition="currentUserAttendee.rsvp" const:value="true">
<var:if condition="currentUserAttendee.partStatWithDefault"
const:value="ACCEPTED" const:negate="YES">
<md-button class="md-raised md-accent" type="button" ng-click="iCalendarAction('accept')">
<var:string label:value="Accept" />
</md-button>
</var:if>
<var:if condition="currentUserAttendee.partStatWithDefault"
const:value="DECLINED" const:negate="YES">
<md-button class="md-raised md-warn" type="button" ng-click="iCalendarAction('decline')">
<var:string label:value="Decline" />
</md-button>
</var:if>
<var:if condition="currentUserAttendee.partStatWithDefault"
const:value="TENTATIVE" const:negate="YES">
<md-button class="md-raised" type="button" ng-click="iCalendarAction('tentative')">
<var:string label:value="Tentative" />
</md-button>
</var:if>
<var:if condition="currentUserAttendee.partStatWithDefault"
const:value="DELEGATED" const:negate="YES">
<md-button class="md-raised" type="button"
ng-click="delegateInvitation = true"
ng-hide="delegateInvitation == true">
<var:string label:value="Delegate ..." />
</md-button>
<div layout="row"
layout-align="start center"
ng-show="delegateInvitation == true">
<md-autocomplete
class="md-flex"
md-search-text="searchText"
md-selected-item="delegatedTo"
md-items="user in userFilter(searchText)"
var:md-min-length="minimumSearchLength"
md-no-cache="true"
md-item-text="user.$shortFormat()"
label:md-floating-label="Delegated to">
<md-item-template>
<span class="md-contact-suggestion" layout="row" layout-align="space-between center">
<span class="md-contact-name"
md-highlight-text="searchText"
md-highlight-flags="^i">{{user.$fullname()}}</span> <span class="md-contact-email"
md-highlight-text="searchText"
md-highlight-flags="^i">{{user.c_email}}</span>
</span>
</md-item-template>
</md-autocomplete>
<md-button class="md-icon-button" ng-click="delegateInvitation = false">
<md-icon>close</md-icon>
</md-button>
<md-button class="md-icon-button md-primary"
ng-click="iCalendarAction('delegate')"
ng-disabled="!delegatedTo || delegatedTo.length == 0">
<md-icon>check</md-icon>
</md-button>
</div>
</var:if>
<!-- TODO - should we re-enable this? It never
worked in v2!
<var:if condition="currentUserAttendee.partStatWithDefault"
const:value="DELEGATED">
<span id="delegateEditor">
<span class="floatLeft"><var:string label:value="Delegated to" />
<a var:href="currentUserAttendee.delegatedTo" name="delegatedToLink" id="delegatedToLink"><var:string value="currentUserAttendee.delegatedTo.rfc822Email" /></a></span>
<input name="delegatedTo" id="delegatedTo" type="text" var:value="currentUserAttendee.delegatedTo.rfc822Email" var:uid="currentUserAttendee.delegatedTo.rfc822Email" style="display: none;" />
<a href="#" class="button actionButton" id="iCalendarDelegate" style="display: none;">
<span><var:string label:value="OK" /></span></a>
</span>
</var:if>
-->
</var:if> <!-- if condition="currentUserAttendee.rsvp" const:value="true" -->
<var:if condition="isEventStoredInCalendar" const:negate="YES">
<md-button class="md-raised" type="button" ng-click="iCalendarAction('addToCalendar')">
<var:string label:value="Add to calendar" />
</md-button>
</var:if>
<md-divider><!-- divider --></md-divider>
</div>
</var:if> <!-- if condition="hasCalendarAccess" -->
<p>
<var:string label:value="Organizer" />
<a ng-non-bindable="" var:href="inEvent.organizer.email"
><var:string value="organizerDisplayName" /></a>
<var:string label:value="request_info" />
</p>
</var:if> <!-- if condition="isLoggedInUserAnAttendee" -->
<var:if condition="isLoggedInUserAnAttendee" const:negate="YES">
<p>
<var:string label:value="Organizer" />
<a ng-non-bindable="" var:href="inEvent.organizer.email">
<var:string value="organizerDisplayName" />
</a>
<var:string label:value="request_info_no_attendee" />
<var:if condition="hasCalendarAccess">
<var:if condition="isEventStoredInCalendar" const:negate="YES">
<!-- Add event to calendar even if the user is not invite -->
<div layout="row" layout-align="start center">
<md-button class="md-raised" type="button" ng-click="iCalendarAction('addToCalendar')">
<var:string label:value="Add to calendar" />
</md-button>
</div>
</var:if>
</var:if>
</p>
</var:if>
</var:if> <!-- if condition="inCalendar.method.uppercaseString"
const:value="REQUEST" -->
<!-- IMIP REPLY - sent to organizer to update the status of the
participant -->
<var:if condition="inCalendar.method.uppercaseString" const:value="REPLY">
<var:if condition="isReplySenderAnAttendee" const:negate="1">
<p>
<var:string label:value="reply_info_no_attendee" />
</p>
</var:if>
<var:if condition="isReplySenderAnAttendee">
<var:if condition="canOriginalEventBeUpdated">
<p>
<var:string label:value="Status Update" />:
<i><var:string label:value="$inReplyAttendee.partStatWithDefault" /></i>,
<var:string label:value="was" />
<i><var:string label:value="$storedReplyAttendee.partStatWithDefault"/></i>.
</p>
<br/>
<md-button class="md-raised" type="button" ng-click="iCalendarAction('updateUserStatus')">
<var:string label:value="Update status" />
</md-button>
</var:if>
</var:if>
</var:if>
<!-- IMIP CANCEL - sent to attendees to notify of the attendee
being removed or the event being deleted -->
<var:if condition="inCalendar.method.uppercaseString" const:value="CANCEL">
<p><var:string label:value="cancel_info_text" /></p>
<var:if condition="isEventStoredInCalendar">
<md-button class="md-raised" type="button" ng-click="iCalendarAction('deleteFromCalendar')">
<var:string label:value="Delete from calendar" />
</md-button>
</var:if>
</var:if>
<!-- IMIP ADD - TODO -->
<var:if condition="inCalendar.method.uppercaseString" const:value="ADD">
<p><var:string label:value="add_info_text" /></p>
</var:if>
<!-- IMIP PUBLISH - none-scheduling event sent to someone for
adding to the calendar -->
<var:if condition="inCalendar.method.uppercaseString" const:value="PUBLISH">
<p><var:string label:value="publish_info_text" /></p>
</var:if>
<var:if condition="isLoggedInUserTheOrganizer">
<!--
Possible Status:
REPLY => check whether it matches, if not suggest change, show comment
REFRESH => add button to resent iCal
COUNTER => show panel to decide on counter
-->
</var:if>
<var:if condition="isLoggedInUserTheOrganizer" const:negate="1">
<!--
Possible Status:
REQUEST => ACCEPT, TENTATIVELY, DECLINE buttons with comment field
- only show buttons for attendees
PUBLISH => just the 'add to calendar' button, rewrite organizer?
ADD / CANCEL
DECLINE-COUNTER
-->
</var:if>
<!-- the user comment is used in replies -->
<var:if condition="userComment.isNotEmpty">
<div class="pseudo-input-container">
<label class="pseudo-input-label">
<var:string label:value="Comment"/>
</label>
<div ng-non-bindable="">
<md-content>
<var:string value="userComment.stringByDetectingURLs" const:insertBR="1" const:escapeHTML="NO"/>
</md-content>
</div>
</div>
</var:if>
<!-- EVENT'S METADATA (time, organizer, attendees, etc.) -->
<var:if condition="hasOrganizer" const:value="true">
<div class="pseudo-input-container">
<label class="pseudo-input-label"><var:string label:value="Organizer"/></label>
<div ng-non-bindable="">
<var:string value="organizerDisplayName"/>
</div>
</div>
</var:if>
<div class="pseudo-input-container">
<label class="pseudo-input-label"><var:string label:value="Time"/></label>
<div>
<var:string value="formattedDateTime"/>
</div>
</div>
<var:if condition="hasLocation">
<div class="pseudo-input-container">
<label class="pseudo-input-label"><var:string label:value="Location"/></label>
<div ng-non-bindable="">
<var:string value="location"/>
</div>
</div>
</var:if>
<!-- ATTENDEES -->
<md-list ng-show="part.participants.length > 0">
<div class="pseudo-input-container">
<label class="pseudo-input-label"><var:string label:value="Attendees"/></label>
<!-- md-contact-chips don't support "readonly", so we build them using md-chips
in readonly mode and a template similar to the one of md-contact-chips -->
<md-chips class="md-contact-chips sg-readonly sg-with-secondary"
ng-model="::part.participants"
readonly="true">
<md-chip-template>
<div class="md-contact-avatar">
<sg-avatar-image sg-email="$chip.email" size="26">person</sg-avatar-image>
</div>
<div class="md-contact-name">{{$chip.name}}</div>
<md-icon class="sg-secondary" ng-class="'icon-' + $chip.partstat"><!-- partstat --></md-icon>
</md-chip-template>
</md-chips>
</div>
</md-list>
<var:if condition="eventDescription.isNotEmpty">
<div class="pseudo-input-container">
<label class="pseudo-input-label">
<var:string label:value="Description"/>
</label>
<div ng-non-bindable="">
<md-content>
<var:string value="eventDescription.stringByDetectingURLs" const:insertBR="1" const:escapeHTML="NO"/>
</md-content>
</div>
</div>
</var:if>
</md-card-content>
</md-card>
</var:if><!--if condition="couldParseCalendar" -->
</div>