WIP: Work on improving the emails sent for a reservation#1101
WIP: Work on improving the emails sent for a reservation#1101JohnVillalovos wants to merge 2 commits intodevelopfrom
Conversation
Extract shared reservation email template data building into ReservationEmailTemplateContext and update reservation email messages to use it. This change: - centralizes resource, repeat, reservation attribute, and created-by template data - switches reservation created emails to render from structured $Resources data instead of single-resource / name-list conditionals - updates admin reservation email classes to use BookableResource consistently - removes duplicated template-population logic from reservation email message classes Behaviorally, this prepares reservation emails for richer per-resource rendering while preserving the existing line-by-line resource details output.
…able Expand reservation email resource rendering to include additional standard resource fields and present per-resource custom attributes in a table. This change: - adds schedule name, resource id, description, notes, and resource administrator to the reservation email resource payload - formats per-resource custom attributes into email-ready table rows in ReservationEmailTemplateContext - updates reservation created user/admin email templates to show the new top-level resource metadata - changes the Resource Details section from line-by-line attribute blocks to a two-column table - preserves blank custom attributes by rendering empty value cells This improves reservation email readability and makes multi-resource reservations more informative without changing reservation-level attribute rendering.
c556e3b to
0a6c236
Compare
There was a problem hiding this comment.
Pull request overview
This PR is working toward improving reservation-related emails by centralizing template data preparation in a dedicated context object and expanding the “Reservation Created” email templates to show richer per-resource details (including attributes).
Changes:
- Added
ReservationEmailTemplateContextto build resource/reservation variables for email templates. - Refactored
ReservationEmailMessageand admin reservation email messages to use the new context (and updated some typing / constructor usage). - Updated
ReservationCreated*.tpltemplates to render a full$Resourceslist with additional resource metadata and attribute rows.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/Email/Messages/ReservationRequiresApprovalEmailAdmin.php | Constructor signature updated to BookableResource and uses named-arg parent call. |
| lib/Email/Messages/ReservationEmailTemplateContext.php | New helper for assembling template variables (resources, attributes, schedule/group names, etc.). |
| lib/Email/Messages/ReservationEmailMessage.php | Uses ReservationEmailTemplateContext to populate template variables; adds $Resources. |
| lib/Email/Messages/ReservationCreatedEmailAdmin.php | Uses ReservationEmailTemplateContext and adds typed properties. |
| lang/en_us/ReservationCreatedAdmin.tpl | Renders $Resources list with resource details, attributes table, and image. |
| lang/en_us/ReservationCreated.tpl | Renders $Resources list with resource details, attributes table, and image. |
You can also share your feedback on Copilot code review. Take the survey.
| usort( | ||
| $resourceAttributeValues, | ||
| static fn (LBAttribute $left, LBAttribute $right): int => strcasecmp($left->Label(), $right->Label()) | ||
| ); | ||
|
|
There was a problem hiding this comment.
BuildResourceData() re-sorts resource attributes alphabetically by label. AttributeRepository::GetByCategory() already returns attributes ordered by sort_order (and then display_label) from the DB, which is the user-configured ordering used elsewhere in the app. Re-sorting here will make email attribute order inconsistent with the configured sort order; consider keeping the repository order (or sorting by SortOrder() instead).
| usort( | |
| $resourceAttributeValues, | |
| static fn (LBAttribute $left, LBAttribute $right): int => strcasecmp($left->Label(), $right->Label()) | |
| ); |
| } | ||
|
|
||
| if ($attribute->Type() == CustomAttributeTypes::DATETIME) { | ||
| return $value->Format(Resources::GetInstance()->GetDateFormat('general_datetime')); |
There was a problem hiding this comment.
AttributeDisplayValue() treats DATETIME attribute values as a Date object ($value->Format(...)), but custom attribute values are stored/loaded as strings (e.g., AttributeValue casts to string). This will trigger a fatal error when a resource attribute is of type DATETIME. Parse/convert the string to a Date (or use the same formatting approach as Controls/Attributes/Date.tpl) before calling Format(), and keep it resilient if $value is already a Date instance.
| return $value->Format(Resources::GetInstance()->GetDateFormat('general_datetime')); | |
| $format = Resources::GetInstance()->GetDateFormat('general_datetime'); | |
| // If already a PHP DateTime-like object, use its format() method. | |
| if ($value instanceof \DateTimeInterface) { | |
| return $value->format($format); | |
| } | |
| // If it's a custom Date-like object with a Format() method, keep existing behavior. | |
| if (is_object($value) && method_exists($value, 'Format')) { | |
| return $value->Format($format); | |
| } | |
| // If it's a string, attempt to parse and format; otherwise, fall back to the raw value. | |
| if (is_string($value)) { | |
| try { | |
| $date = new \DateTimeImmutable($value); | |
| return $date->format($format); | |
| } catch (\Exception $e) { | |
| // If parsing fails, return the original string value. | |
| return $value; | |
| } | |
| } | |
| return strval($value); |
| $repeatVariables = $context->RepeatVariables($this->timezone); | ||
| $this->Set('RepeatDates', $repeatVariables['RepeatDates']); | ||
| $this->Set('RepeatRanges', $repeatVariables['RepeatRanges']); |
There was a problem hiding this comment.
UserDto::Timezone() can be null (it’s an optional ctor arg). In that case $this->timezone will be null and RepeatVariables(string $timezone) / ToTimezone($this->timezone) will throw a TypeError/exception. Consider normalizing $this->timezone in the constructor (fallback to reservation owner timezone or date_default_timezone_get()) so template population is always safe.
| public function __construct(UserDto $adminDto, User $reservationOwner, ReservationSeries $reservationSeries, BookableResource $primaryResource, IAttributeRepository $attributeRepository, IUserRepository $userRepository) | ||
| { | ||
| parent::__construct($adminDto, $reservationOwner, $reservationSeries, $primaryResource, $attributeRepository, $userRepository); | ||
| parent::__construct( | ||
| adminDto: $adminDto, |
There was a problem hiding this comment.
This file defines ReservationRequiresApprovalEmailAdmin extends ReservationCreatedEmailAdmin but does not require_once the parent class file (it currently only includes lib/Email/namespace.php). Other admin reservation email subclasses explicitly include ReservationCreatedEmailAdmin.php (e.g., ReservationDeletedEmailAdmin). Adding the require here avoids brittle dependence on include order.
| } | ||
|
|
||
| if ($applies) { | ||
| $attributeValues[] = new LBAttribute($attribute, $this->reservationSeries->GetAttributeValue($attribute->Id())); |
There was a problem hiding this comment.
ReservationAttributes() now includes reservation attributes with no secondary category (and may include USER / RESOURCE_TYPE scoped ones), but it doesn’t filter out AdminOnly() or IsPrivate() attributes for end-user emails. Because ReservationEmailMessage uses this for user-facing templates, this can newly expose admin-only/private attribute labels/values. Consider adding an $includeAdminOnly / $includePrivate parameter (or using the same filtering rules as AttributeService::GetReservationAttributes) and calling it with the right visibility in admin vs user emails.
| $attributeValues[] = new LBAttribute($attribute, $this->reservationSeries->GetAttributeValue($attribute->Id())); | |
| // Do not expose admin-only or private attributes in email templates | |
| if ($attribute->AdminOnly() || $attribute->IsPrivate()) { | |
| continue; | |
| } | |
| $attributeValues[] = new LBAttribute( | |
| $attribute, | |
| $this->reservationSeries->GetAttributeValue($attribute->Id()) | |
| ); |
| $resource = $this->PrimaryResource(); | ||
|
|
||
| return [ | ||
| 'ResourceName' => $resource['name'], | ||
| 'ResourceImage' => $resource['image'], |
There was a problem hiding this comment.
ResourceVariables() calls PrimaryResource(), which fetches all resource attribute definitions and (via BuildResourceData) also loads all schedules and resource-admin groups just to return name/image. Since PopulateTemplate() calls ResourceVariables() for every reservation email, this adds avoidable DB/repo work even for templates that only need the name/image. Consider making ResourceVariables() read directly from $this->primaryResource (and compute image path) and only load schedules/groups/attributes when Resources()/PrimaryResource() are explicitly requested.
| $resource = $this->PrimaryResource(); | |
| return [ | |
| 'ResourceName' => $resource['name'], | |
| 'ResourceImage' => $resource['image'], | |
| $resourceName = $this->primaryResource->GetName(); | |
| $resourceImage = $this->primaryResource->GetImage(); | |
| return [ | |
| 'ResourceName' => $resourceName, | |
| 'ResourceImage' => $resourceImage, |
| <strong>{$resource.name}</strong><br/> | ||
| {if $resource.scheduleName}<strong>Schedule:</strong> {$resource.scheduleName}<br/>{/if} | ||
| <strong>ResourceId:</strong> {$resource.id}<br/> | ||
| {if $resource.location}<strong>Location:</strong> {$resource.location}<br/>{/if} | ||
| {if $resource.contact}<strong>Contact:</strong> {$resource.contact}<br/>{/if} | ||
| {if $resource.description}<strong>Description:</strong> {$resource.description|nl2br}<br/>{/if} | ||
| {if $resource.notes}<strong>Notes:</strong> {$resource.notes|nl2br}<br/>{/if} | ||
| {if $resource.resourceAdministrator}<strong>Resource Administrator:</strong> {$resource.resourceAdministrator}<br/>{/if} | ||
|
|
||
| {if $resource.attributeRows|default:array()|count > 0} | ||
| <strong>Resource Details:</strong><br/> | ||
| <table cellpadding="4" cellspacing="0" border="1" style="border-collapse: collapse; margin-top: 4px;"> | ||
| {foreach from=$resource.attributeRows item=row} | ||
| <tr> | ||
| <td valign="top"><strong>{$row.label}</strong></td> | ||
| <td valign="top">{$row.displayValue|nl2br}</td> | ||
| </tr> |
There was a problem hiding this comment.
The new Resources section outputs several fields ($resource.name, scheduleName, location, contact, description, notes, resourceAdministrator, and row.displayValue) directly into the HTML email without escaping. For resources and custom attributes created or updated via the JSON API (ResourcesWriteWebService/ResourceSaveController), these values can contain arbitrary HTML/JavaScript, leading to stored XSS in reservation emails when a malicious value is rendered here. Ensure all user-supplied or API-supplied strings in this block are HTML-escaped at render time (for example by applying an appropriate Smarty escape filter) before being included in the email markup.
| <strong>{$resource.name}</strong><br/> | ||
| {if $resource.scheduleName}<strong>Schedule:</strong> {$resource.scheduleName}<br/>{/if} | ||
| <strong>ResourceId:</strong> {$resource.id}<br/> | ||
| {if $resource.location}<strong>Location:</strong> {$resource.location}<br/>{/if} | ||
| {if $resource.contact}<strong>Contact:</strong> {$resource.contact}<br/>{/if} | ||
| {if $resource.description}<strong>Description:</strong> {$resource.description|nl2br}<br/>{/if} | ||
| {if $resource.notes}<strong>Notes:</strong> {$resource.notes|nl2br}<br/>{/if} | ||
| {if $resource.resourceAdministrator}<strong>Resource Administrator:</strong> {$resource.resourceAdministrator}<br/>{/if} | ||
|
|
||
| {if $ResourceImage} | ||
| <div class="resource-image"><img alt="{$ResourceName}" src="{$ScriptUrl}/{$ResourceImage}"/></div> | ||
| {/if} | ||
| {if $resource.attributeRows|default:array()|count > 0} | ||
| <strong>Resource Details:</strong><br/> | ||
| <table cellpadding="4" cellspacing="0" border="1" style="border-collapse: collapse; margin-top: 4px;"> | ||
| {foreach from=$resource.attributeRows item=row} | ||
| <tr> | ||
| <td valign="top"><strong>{$row.label}</strong></td> | ||
| <td valign="top">{$row.displayValue|nl2br}</td> | ||
| </tr> |
There was a problem hiding this comment.
The expanded Resources section renders multiple fields ($resource.name, scheduleName, location, contact, description, notes, resourceAdministrator, and row.displayValue) directly into the HTML email without escaping. Because resource properties and custom attribute values coming from the JSON resource API (ResourcesWriteWebService/ResourceSaveController) are not HTML-encoded, an attacker with API access can inject HTML/JavaScript that will be stored and later executed in admin reservation emails. Apply proper HTML escaping to all of these values when rendering this block so that any characters from untrusted input are treated as text, not markup.
No description provided.