MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
This commit is contained in:
@@ -0,0 +1,956 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MRBS\ICalendar\Calendar;
|
||||
use MRBS\ICalendar\CalendarException;
|
||||
use MRBS\ICalendar\Event;
|
||||
use MRBS\ICalendar\Timezone;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
// +---------------------------------------------------------------------------+
|
||||
// | Meeting Room Booking System. |
|
||||
// +---------------------------------------------------------------------------+
|
||||
// | Functions dedicated to emails handling. |
|
||||
// |---------------------------------------------------------------------------+
|
||||
// | I keeped these functions in a separated file to avoid burden the main |
|
||||
// | function.inc files if emails are not used. |
|
||||
// | |
|
||||
// | USE : This file should be included in all files where emails functions |
|
||||
// | are likely to be used. |
|
||||
// +---------------------------------------------------------------------------+
|
||||
//
|
||||
|
||||
define('MAIL_EOL', "\r\n"); // See RFC 5322 2.1
|
||||
|
||||
// Determines whether an email might need to be sent
|
||||
function need_to_send_mail() : bool
|
||||
{
|
||||
global $mail_settings;
|
||||
|
||||
return ($mail_settings['admin_on_bookings'] or
|
||||
$mail_settings['area_admin_on_bookings'] or
|
||||
$mail_settings['room_admin_on_bookings'] or
|
||||
$mail_settings['booker'] or
|
||||
$mail_settings['book_admin_on_approval']);
|
||||
}
|
||||
|
||||
|
||||
// Get localized (for email) field name for a user defined table column
|
||||
// Looks for a tag of the format tablename.columnname (where tablename is
|
||||
// stripped of the table prefix) and if it can't find a string for that tag will
|
||||
// return the column name
|
||||
// TODO: It's actually returning tablename.columnname at the moment if it
|
||||
// TODO: can't find a tag, rather than just the columnname. Probably need
|
||||
// TODO: to restructure the way get_vocab() etc work.
|
||||
function get_mail_field_name(string $table, string $name) : string
|
||||
{
|
||||
return get_mail_vocab(get_table_short_name($table) . ".$name");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format a timestamp in non-unicode output (for emails).
|
||||
*
|
||||
* @param int $t timestamp to format
|
||||
* @param boolean $inc_time include time in return string
|
||||
* @return string formatted string
|
||||
*/
|
||||
function getMailTimeDateString(int $t, bool $inc_time=true) : string
|
||||
{
|
||||
global $datetime_formats;
|
||||
|
||||
$format = ($inc_time) ? $datetime_formats['date_and_time'] : $datetime_formats['date'];
|
||||
return datetime_format($format, $t, Language::getInstance()->getMailLocale());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the date string for a timestamp, in the mail locale.
|
||||
*
|
||||
* @param bool $is_end_time Whether the timestamp is for an end time or not. This makes a difference for periods as
|
||||
* the end time is then the name of the previous period.
|
||||
*/
|
||||
function getMailDateString(int $t, bool $is_end_time=false) : string
|
||||
{
|
||||
global $area, $enable_periods;
|
||||
|
||||
if ($enable_periods)
|
||||
{
|
||||
$entry_date = period_date_string($t, $area, $is_end_time, Language::getInstance()->getMailLocale());
|
||||
}
|
||||
else
|
||||
{
|
||||
$entry_date = getMailTimeDateString($t);
|
||||
}
|
||||
return $entry_date;
|
||||
}
|
||||
|
||||
|
||||
// get_address_list($array)
|
||||
//
|
||||
// Takes an array of email addresses and returns a comma separated
|
||||
// list of addresses with duplicates removed.
|
||||
function get_address_list(array $address_strings) : string
|
||||
{
|
||||
// Remove any leading and trailing whitespace and any empty strings
|
||||
$trimmed_array = array();
|
||||
foreach ($address_strings as $address_string)
|
||||
{
|
||||
$address_string = trim($address_string);
|
||||
if ($address_string !== '')
|
||||
{
|
||||
// Use parse_addresses to validate the address because it could contain a display name
|
||||
if (count(parse_addresses($address_string)) == 0)
|
||||
{
|
||||
$message = 'Invalid email address "' . $address_string . '"';
|
||||
mail_debug($message);
|
||||
trigger_error($message, E_USER_NOTICE);
|
||||
}
|
||||
$trimmed_array[] = $address_string;
|
||||
}
|
||||
}
|
||||
// remove duplicates
|
||||
$trimmed_array = array_unique($trimmed_array);
|
||||
// re-assemble the string
|
||||
return implode(',', $trimmed_array);
|
||||
}
|
||||
|
||||
|
||||
// Get the admin email address(es), provided that the config settings allow it.
|
||||
function get_admin_email() : string
|
||||
{
|
||||
global $mail_settings;
|
||||
|
||||
return ($mail_settings['admin_on_bookings'] && isset($mail_settings['recipients'])) ? $mail_settings['recipients'] : '';
|
||||
}
|
||||
|
||||
|
||||
// get the list of email addresses that are allowed to approve bookings
|
||||
// for the room with id $room_id
|
||||
// (At the moment this is just the admin email address, but this could
|
||||
// be extended.)
|
||||
function get_approvers_email($room_id) : string
|
||||
{
|
||||
return get_admin_email();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the area admin email address(es) for the booking.
|
||||
*
|
||||
* @param array $data The data for the booking (could be an individual entry or a series)
|
||||
*
|
||||
* @return string The area admin email address for the booking, or an empty string if there is none.
|
||||
*/
|
||||
function get_area_admin_email(array $data) : string
|
||||
{
|
||||
// Sometimes the data will already contain the area admin email address.
|
||||
if (isset($data['area_admin_email']) && ($data['area_admin_email'] !== ''))
|
||||
{
|
||||
return $data['area_admin_email'];
|
||||
}
|
||||
|
||||
// Otherwise, get it from the database.
|
||||
$id_table = ($data['repeat_rule']->getType() === RepeatRule::NONE) ? 'E' : 'T';
|
||||
|
||||
$sql = "SELECT A.area_admin_email
|
||||
FROM " . _tbl('room') . " M, " . _tbl('area') . " A, ";
|
||||
|
||||
$sql .= ($id_table == 'E') ? _tbl('entry') . " E " : _tbl('repeat') . " T ";
|
||||
$sql .= "WHERE {$id_table}.id=:id
|
||||
AND M.id={$id_table}.room_id
|
||||
AND A.id=M.area_id
|
||||
LIMIT 1";
|
||||
$email = db()->query_scalar_non_bool($sql, [':id' => $data['id']]);
|
||||
|
||||
return (!isset($email) || ($email === false)) ? '' : $email;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the room admin email address(es) for the booking.
|
||||
*
|
||||
* @param array $data The data for the booking (could be an individual entry or a series)
|
||||
*
|
||||
* @return string The area admin email address for the booking, or an empty string if there is none.
|
||||
*/
|
||||
function get_room_admin_email(array $data) : string
|
||||
{
|
||||
// Sometimes the data will already contain the room admin email address.
|
||||
if (isset($data['room_admin_email']) && ($data['room_admin_email'] !== ''))
|
||||
{
|
||||
return $data['room_admin_email'];
|
||||
}
|
||||
|
||||
// Otherwise, get it from the database.
|
||||
$id_table = ($data['repeat_rule']->getType() === RepeatRule::NONE) ? "E" : "T";
|
||||
|
||||
$sql = "SELECT M.room_admin_email
|
||||
FROM " . _tbl('room') . " M, ";
|
||||
$sql .= ($id_table == 'E') ? _tbl('entry') . " E " : _tbl('repeat') . " T ";
|
||||
$sql .= "WHERE {$id_table}.id=:id
|
||||
AND M.id={$id_table}.room_id
|
||||
LIMIT 1";
|
||||
$email = db()->query_scalar_non_bool($sql, [':id' => $data['id']]);
|
||||
|
||||
return (!isset($email) || ($email === false)) ? '' : $email;
|
||||
}
|
||||
|
||||
|
||||
// Create a row of a table in either plain text or HTML format.
|
||||
// Plain text: returns "$label: $new" . MAIL_EOL
|
||||
// HTML: returns "<tr><td>$label: </td><td>$new</td></tr>" . MAIL_EOL
|
||||
// $new and $old can be of type null|int|float|string (union declarations are not supported until PHP 8.0).
|
||||
// If $compare is TRUE then a third column is output with $old in parentheses
|
||||
function create_body_table_row(string $label, $new, $old, bool $compare=false, bool $as_html=false) : string
|
||||
{
|
||||
$new = $new ?? '';
|
||||
$old = $old ?? '';
|
||||
|
||||
$result = ($as_html) ? '<tr>' . MAIL_EOL : '';
|
||||
|
||||
// The label
|
||||
$result .= ($as_html) ? '<td>' : '';
|
||||
$result .= ($as_html) ? escape_html("$label: ") : "$label: ";
|
||||
$result .= ($as_html) ? '</td>' . MAIL_EOL : '';
|
||||
// The new value
|
||||
$result .= ($as_html) ? '<td>' : '';
|
||||
$result .= ($as_html) ? escape_html($new) : "$new";
|
||||
$result .= ($as_html) ? '</td>' . MAIL_EOL : '';
|
||||
// The old value (if we're doing a comparison)
|
||||
if ($compare)
|
||||
{
|
||||
$result .= ($as_html) ? '<td>' : '';
|
||||
if ($new == $old)
|
||||
{
|
||||
$result .= ($as_html) ? " " : '';
|
||||
}
|
||||
else
|
||||
{
|
||||
// Put parentheses around the HTML version as well as the plain text
|
||||
// version in case the table is not rendered properly in HTML. The
|
||||
// parentheses will make the old value stand out.
|
||||
$result .= ($as_html) ? escape_html(" ($old)") : " ($old)";
|
||||
}
|
||||
$result .= ($as_html) ? '</td>' . MAIL_EOL : '';
|
||||
}
|
||||
|
||||
$result .= ($as_html) ? '</tr>' : '';
|
||||
$result .= MAIL_EOL;
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Generate a list of dates from an array of start times
|
||||
//
|
||||
// $dates an array of start times
|
||||
// $as_html (boolean) whether the list should be HTML or plain text
|
||||
function create_date_list(array $dates, bool $as_html) : string
|
||||
{
|
||||
$result = ($as_html) ? '<ul>' . MAIL_EOL : '';
|
||||
foreach ($dates as $date)
|
||||
{
|
||||
$result .= ($as_html) ? '<li>' : '';
|
||||
$date_string = getMailDateString($date);
|
||||
$result .= ($as_html) ? escape_html($date_string) : $date_string;
|
||||
$result .= ($as_html) ? '</li>' : '';
|
||||
// The newline is important to stop the line length exceeding 998 characters,
|
||||
// which will happen if there are a lot of dates. See RFC 5322 2.1.1.
|
||||
$result .= MAIL_EOL;
|
||||
}
|
||||
$result .= ($as_html) ? '</ul>' . MAIL_EOL : '';
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Generate a list of repeat dates for a series
|
||||
//
|
||||
// $reps is an array of start_times that have been created/modified/deleted.
|
||||
function create_repeat_list(array $data, $action, bool $as_html, array $reps) : string
|
||||
{
|
||||
if (($data['repeat_rule']->getType() == RepeatRule::NONE) ||
|
||||
in_array($action, array('more_info', 'remind')))
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
// The introductory text
|
||||
$result = ($as_html) ? '<p>' : MAIL_EOL . MAIL_EOL;
|
||||
if (($action == "delete") || ($action == "reject"))
|
||||
{
|
||||
$result .= get_vocab("mail_body_repeats_deleted");
|
||||
}
|
||||
else
|
||||
{
|
||||
$result .= get_vocab("mail_body_repeats_booked");
|
||||
}
|
||||
$result .= ($as_html) ? '</p>' . MAIL_EOL : MAIL_EOL . MAIL_EOL;
|
||||
|
||||
$result .= create_date_list($reps, $as_html);
|
||||
|
||||
// Now add in the list of repeat bookings that could not be booked
|
||||
if (!empty($data['skip_list']))
|
||||
{
|
||||
// The introductory text
|
||||
$result .= ($as_html) ? '<p>' : MAIL_EOL . MAIL_EOL;
|
||||
$result .= get_vocab("mail_body_exceptions");
|
||||
$result .= ($as_html) ? '</p>' . MAIL_EOL : MAIL_EOL . MAIL_EOL;
|
||||
// Now the list of conflicts
|
||||
$result .= create_date_list($data['skip_list'], $as_html);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// $start_times is an array of start_times that have been created/modified/deleted.
|
||||
// If not specified the function works them out for itself from the repeat data
|
||||
function create_body(array $data, ?array $mail_previous, bool $series, string $action, array $start_times, bool $as_html=false, ?string $note=null) : string
|
||||
{
|
||||
global $mrbs_company;
|
||||
global $enable_periods, $approval_enabled, $confirmation_enabled;
|
||||
global $mail_settings, $standard_fields;
|
||||
global $select_options, $booking_types;
|
||||
|
||||
$compare = !empty($mail_previous);
|
||||
|
||||
// If we haven't got a previous entry just give it one. It won't get used,
|
||||
// but will prevent a series if undefined index notices.
|
||||
if (empty($mail_previous))
|
||||
{
|
||||
$mail_previous = $data;
|
||||
}
|
||||
|
||||
// set up the body
|
||||
$body = "";
|
||||
|
||||
if ($as_html)
|
||||
{
|
||||
$body .= DOCTYPE . MAIL_EOL;
|
||||
$body .= '<html lang="' . Language::getInstance()->getMailLang() . '">' . MAIL_EOL;
|
||||
$body .= '<head>' . MAIL_EOL;
|
||||
$body .= '<meta http-equiv="Content-Type" content="text/html; charset=' . Language::MAIL_CHARSET . '">' . MAIL_EOL;
|
||||
$body .= '<title>' . escape_html($mrbs_company) . '</title>' . MAIL_EOL;
|
||||
$body .= '<style type="text/css">' . MAIL_EOL;
|
||||
$css_file = 'css/mrbs-mail.css.php';
|
||||
if (is_file($css_file) && is_readable($css_file))
|
||||
{
|
||||
ob_start();
|
||||
include $css_file;
|
||||
$css = ob_get_clean();
|
||||
// Remove any whitespace from the beginning
|
||||
$css = preg_replace('/^\s+/', '', $css);
|
||||
// Remove comments
|
||||
$css = preg_replace('!/\*.*?\*/!s', '', $css);
|
||||
// Remove blank lines and also replace all new line sequences with the preferred
|
||||
// EOL sequence - hence the '+'. Note that the CSS file will probably have Unix LF
|
||||
// endings, so these will need to be converted.
|
||||
$css = preg_replace("/(?:\R\h*)+/", MAIL_EOL, $css);
|
||||
$body .= $css;
|
||||
}
|
||||
$body .= '</style>' . MAIL_EOL;
|
||||
$body .= '</head>' . MAIL_EOL;
|
||||
$body .= '<body id="mrbs">' . MAIL_EOL;
|
||||
$body .= '<div id="header">' . escape_html($mrbs_company . ' - ' . get_mail_vocab('mrbs')) . '</div>' . MAIL_EOL;
|
||||
$body .= '<div id="contents">' . MAIL_EOL;
|
||||
}
|
||||
|
||||
$body .= ($as_html) ? "<p>" : "";
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
$user_escaped = ($as_html) ? escape_html($mrbs_user->display_name) : $mrbs_user->display_name;
|
||||
}
|
||||
else
|
||||
{
|
||||
$user_escaped = '';
|
||||
}
|
||||
|
||||
switch ($action)
|
||||
{
|
||||
case "approve":
|
||||
$body .= get_mail_vocab("mail_body_approved", $user_escaped);
|
||||
break;
|
||||
case "more_info":
|
||||
$body .= get_mail_vocab("mail_body_more_info", $user_escaped);
|
||||
$body .= ($as_html) ? '</p><p>' : MAIL_EOL . MAIL_EOL;
|
||||
$body .= get_mail_vocab("info_requested") . ": ";
|
||||
$body .= $note ?? '';
|
||||
break;
|
||||
case "remind":
|
||||
$body .= get_mail_vocab("mail_body_reminder");
|
||||
break;
|
||||
case "reject":
|
||||
$body .= get_mail_vocab("mail_body_rej_entry", $user_escaped);
|
||||
$body .= ($as_html) ? '</p><p>' : MAIL_EOL . MAIL_EOL;
|
||||
$body .= get_mail_vocab("reason") . ': ';
|
||||
$body .= $note ?? '';
|
||||
break;
|
||||
case "delete":
|
||||
$body .= get_mail_vocab("mail_body_del_entry", $user_escaped);
|
||||
break;
|
||||
default:
|
||||
if ($compare)
|
||||
{
|
||||
$body .= get_mail_vocab("mail_body_changed_entry", $user_escaped);
|
||||
}
|
||||
else
|
||||
{
|
||||
$body .= get_mail_vocab("mail_body_new_entry", $user_escaped);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Create a link to the entry, unless we're deleting it of course,
|
||||
// because then there won't be one.
|
||||
if (($action != "delete") && ($action != "reject"))
|
||||
{
|
||||
$body .= ($as_html) ? '</p><p>' : MAIL_EOL . MAIL_EOL;
|
||||
$body .= ($as_html) ? '<a target="_blank" href="' : '';
|
||||
// Set the link to view entry page
|
||||
$body .= url_base() . multisite('view_entry.php?id=' . $data['id']);
|
||||
if ($series)
|
||||
{
|
||||
$body .= '&series=1';
|
||||
}
|
||||
$body .= ($as_html) ? '">' . escape_html($data['name']) . '</a>' : '';
|
||||
}
|
||||
$body .= ($as_html) ? '</p>' . MAIL_EOL : MAIL_EOL . MAIL_EOL;
|
||||
|
||||
$body .= ($as_html) ? '<table>' : '';
|
||||
$body .= MAIL_EOL;
|
||||
|
||||
if ($compare && $as_html)
|
||||
{
|
||||
$body .= '<thead>' . MAIL_EOL;
|
||||
$body .= '<tr>' . MAIL_EOL;
|
||||
$body .= '<th> </th>' . MAIL_EOL;
|
||||
$body .= '<th>' . get_vocab("new_value") . '</th>' . MAIL_EOL;
|
||||
$body .= '<th>(' . get_vocab("old_value") . ')</th>' . MAIL_EOL;
|
||||
$body .= '</tr>' . MAIL_EOL;
|
||||
$body .= '</thead>' . MAIL_EOL;
|
||||
}
|
||||
|
||||
$body .= ($as_html) ? '<tbody>' . MAIL_EOL : '';
|
||||
|
||||
|
||||
// Always display the brief description
|
||||
$body .= create_body_table_row (get_mail_vocab("namebooker"),
|
||||
$data['name'],
|
||||
$mail_previous['name'],
|
||||
$compare, $as_html);
|
||||
|
||||
// Displays/don't displays entry details
|
||||
if ($mail_settings['details'])
|
||||
{
|
||||
// Description:
|
||||
$body .= create_body_table_row (get_mail_vocab("description"),
|
||||
$data['description'],
|
||||
$mail_previous['description'],
|
||||
$compare, $as_html);
|
||||
|
||||
if ($confirmation_enabled)
|
||||
{
|
||||
// Confirmation status:
|
||||
$new_status = ($data['tentative']) ? get_mail_vocab("tentative") : get_mail_vocab("confirmed");
|
||||
$old_status = ($mail_previous['tentative']) ? get_mail_vocab("tentative") : get_mail_vocab("confirmed");
|
||||
$body .= create_body_table_row (get_mail_vocab("confirmation_status"),
|
||||
$new_status,
|
||||
$old_status,
|
||||
$compare, $as_html);
|
||||
}
|
||||
|
||||
if ($approval_enabled)
|
||||
{
|
||||
// Approval status:
|
||||
$new_status = ($data['awaiting_approval']) ? get_mail_vocab("awaiting_approval") : get_mail_vocab("approved");
|
||||
$old_status = ($mail_previous['awaiting_approval']) ? get_mail_vocab("awaiting_approval") : get_mail_vocab("approved");
|
||||
$body .= create_body_table_row (get_mail_vocab("approval_status"),
|
||||
$new_status,
|
||||
$old_status,
|
||||
$compare, $as_html);
|
||||
}
|
||||
|
||||
// Room:
|
||||
$new_room = $data['area_name'] . " - " . $data['room_name'];
|
||||
$old_room = $mail_previous['area_name'] . " - " . $mail_previous['room_name'];
|
||||
$body .= create_body_table_row (get_mail_vocab("room"),
|
||||
$new_room,
|
||||
$old_room,
|
||||
$compare, $as_html);
|
||||
|
||||
// Start time
|
||||
$body .= create_body_table_row (get_mail_vocab("start_date"),
|
||||
getMailDateString($data['start_time']),
|
||||
getMailDateString($mail_previous['start_time']),
|
||||
$compare, $as_html);
|
||||
|
||||
// Duration
|
||||
$new_duration = $data['duration'] . " " . get_mail_vocab($data['dur_units']);
|
||||
$old_duration = $mail_previous['duration'] . " " . get_mail_vocab($mail_previous['dur_units']);
|
||||
$body .= create_body_table_row (get_mail_vocab("duration"),
|
||||
$new_duration,
|
||||
$old_duration,
|
||||
$compare, $as_html);
|
||||
|
||||
// End time
|
||||
$this_endtime = $data['end_time'];
|
||||
$previous_endtime = ($compare) ? $mail_previous['end_time'] : 0;
|
||||
$body .= create_body_table_row (get_mail_vocab("end_date"),
|
||||
getMailDateString($this_endtime, true),
|
||||
getMailDateString($previous_endtime, true),
|
||||
$compare, $as_html);
|
||||
|
||||
// Type of booking
|
||||
if (isset($booking_types) && (count($booking_types) > 1))
|
||||
{
|
||||
$body .= create_body_table_row (get_mail_vocab("type"),
|
||||
get_type_vocab($data['type']),
|
||||
get_type_vocab($mail_previous['type']),
|
||||
$compare, $as_html);
|
||||
}
|
||||
|
||||
// Created by
|
||||
$body .= create_body_table_row (get_mail_vocab("createdby"),
|
||||
auth()->getDisplayName($data['create_by']),
|
||||
auth()->getDisplayName($mail_previous['create_by']),
|
||||
$compare, $as_html);
|
||||
|
||||
// Custom fields
|
||||
$columns = Columns::getInstance(_tbl('entry'));
|
||||
foreach ($columns as $column)
|
||||
{
|
||||
if (!in_array($column->name, $standard_fields['entry']))
|
||||
{
|
||||
$key = $column->name;
|
||||
$value = $data[$key];
|
||||
// Convert any booleans or pseudo-booleans to text strings (in the mail language)
|
||||
if ($column->isBooleanLike())
|
||||
{
|
||||
$value = ($value) ? get_mail_vocab("yes") : get_mail_vocab("no");
|
||||
if ($compare)
|
||||
{
|
||||
$mail_previous[$key] = ($mail_previous[$key]) ? get_mail_vocab("yes") : get_mail_vocab("no");
|
||||
}
|
||||
}
|
||||
// For any associative arrays we want the value rather than the key
|
||||
if (isset($select_options["entry.$key"]) &&
|
||||
is_assoc($select_options["entry.$key"]))
|
||||
{
|
||||
if (isset($value) && array_key_exists($value, $select_options["entry.$key"]))
|
||||
{
|
||||
$value = $select_options["entry.$key"][$value];
|
||||
}
|
||||
if ($compare &&
|
||||
isset($mail_previous[$key]) &&
|
||||
array_key_exists($mail_previous[$key], $select_options["entry.$key"]))
|
||||
{
|
||||
$mail_previous[$key] = $select_options["entry.$key"][$mail_previous[$key]];
|
||||
}
|
||||
}
|
||||
$body .= create_body_table_row (get_mail_field_name(_tbl('entry'), $key),
|
||||
$value,
|
||||
($compare) ? $mail_previous[$key] : '',
|
||||
$compare, $as_html);
|
||||
}
|
||||
}
|
||||
|
||||
// Last updated
|
||||
$body .= create_body_table_row (get_mail_vocab("lastupdate"),
|
||||
getMailTimeDateString(time()),
|
||||
($compare) ? getMailTimeDateString($mail_previous['last_updated']) : '',
|
||||
$compare, $as_html);
|
||||
|
||||
// Repeat Type
|
||||
$body .= create_body_table_row (get_mail_vocab("rep_type"),
|
||||
get_mail_vocab("rep_type_" . $data['repeat_rule']->getType()),
|
||||
get_mail_vocab("rep_type_" . $mail_previous['repeat_rule']->getType()),
|
||||
$compare, $as_html);
|
||||
|
||||
// Details if a series
|
||||
if ($data['repeat_rule']->getType() != RepeatRule::NONE)
|
||||
{
|
||||
|
||||
if ($data['repeat_rule']->getType() == RepeatRule::WEEKLY)
|
||||
{
|
||||
// Repeat days
|
||||
// Display day names according to language and preferred weekday start.
|
||||
$opt = $data['repeat_rule']->getDaysAsNames(true);
|
||||
$opt_previous = ($compare) ? $mail_previous['repeat_rule']->getDaysAsNames(true) : '';
|
||||
$body .= create_body_table_row (get_mail_vocab("rep_rep_day"),
|
||||
$opt,
|
||||
$opt_previous,
|
||||
$compare, $as_html);
|
||||
}
|
||||
|
||||
if ($data['repeat_rule']->getType() == RepeatRule::MONTHLY)
|
||||
{
|
||||
$previous_repeat_day = ($mail_previous['repeat_rule']->getType() == RepeatRule::MONTHLY) ? get_monthly_repeat_day($mail_previous) : '';
|
||||
$body .= create_body_table_row (get_mail_vocab("repeat_on"),
|
||||
get_monthly_repeat_day($data),
|
||||
$previous_repeat_day,
|
||||
$compare, $as_html);
|
||||
}
|
||||
|
||||
// Repeat interval
|
||||
$repeat_interval = $data['repeat_rule']->getInterval();
|
||||
$new = $repeat_interval . ' ' . $data['repeat_rule']->getIntervalUnits(true);
|
||||
|
||||
$previous_repeat_interval = $mail_previous['repeat_rule']->getInterval();
|
||||
if (isset($previous_repeat_interval))
|
||||
{
|
||||
$old = $previous_repeat_interval . ' ' . $mail_previous['repeat_rule']->getIntervalUnits(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
$old = '';
|
||||
}
|
||||
|
||||
$body .= create_body_table_row (get_mail_vocab("rep_interval"),
|
||||
$new,
|
||||
$old,
|
||||
$compare, $as_html);
|
||||
|
||||
// Repeat end date
|
||||
$end_previous = ($mail_previous['repeat_rule']->getType() == RepeatRule::NONE) ? '' : getMailTimeDateString($mail_previous['repeat_rule']->getEndDate()->getTimestamp(), false);
|
||||
$body .= create_body_table_row (get_mail_vocab("rep_end_date"),
|
||||
getMailTimeDateString($data['repeat_rule']->getEndDate()->getTimestamp(), false),
|
||||
$end_previous,
|
||||
$compare, $as_html);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if ($as_html)
|
||||
{
|
||||
$body .= '</tbody>' . MAIL_EOL;
|
||||
$body .= '</table>' . MAIL_EOL;
|
||||
}
|
||||
|
||||
// Add in a list of repeat dates. Although we've given them the repeat characteristics
|
||||
// above, it's often helpful to have this expanded out into a list of actual dates to
|
||||
// avoid any confusion. The repeat list also gives a list of dates that could not
|
||||
// be booked due to conflicts.
|
||||
if ($data['repeat_rule']->getType() != RepeatRule::NONE)
|
||||
{
|
||||
$body .= create_repeat_list($data, $action, $as_html, $start_times);
|
||||
}
|
||||
|
||||
if ($as_html)
|
||||
{
|
||||
$body .= '</div>' . MAIL_EOL;
|
||||
$body .= '</body>' . MAIL_EOL;
|
||||
$body .= '</html>' . MAIL_EOL;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
|
||||
// Merges a comma separated string of addresses (could be a single address) into
|
||||
// an array of addresses
|
||||
// TODO: simplify all address handling
|
||||
function address_merge(array $addresses, string $address_string) : array
|
||||
{
|
||||
$result = $addresses;
|
||||
|
||||
$new_addresses = parse_addresses($address_string);
|
||||
$mailer = new PHPMailer();
|
||||
$mailer->CharSet = Language::MAIL_CHARSET;
|
||||
|
||||
foreach ($new_addresses as $new_address)
|
||||
{
|
||||
$result[] = $mailer->addrFormat(array($new_address['address'], $new_address['name']));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// create_addresses($data, $action)
|
||||
//
|
||||
// Returns an array indexed by 'from', 'reply_to', 'to' and 'cc' with each element
|
||||
// consisting of a comma separated list of email addresses.
|
||||
//
|
||||
// Parameters:
|
||||
// $data an array containing all the data concerning this booking
|
||||
// $action the action that has caused this email to be sent
|
||||
//
|
||||
function create_addresses(array $data, array $previous, string $action) : array
|
||||
{
|
||||
global $approval_enabled, $mail_settings;
|
||||
|
||||
$reply_to = array();
|
||||
$to = array();
|
||||
$cc = array();
|
||||
|
||||
if (!empty($mail_settings['cc']))
|
||||
{
|
||||
$cc = address_merge($cc, $mail_settings['cc']);
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
// Set the Reply-To address
|
||||
if ($mail_settings['use_reply_to'] && !empty($mrbs_user->email))
|
||||
{
|
||||
$reply_to[] = $mrbs_user->mailbox();
|
||||
}
|
||||
// Set the From address. If this is a reminder email or a request for more info,
|
||||
// then set the From address to be the user's - unless we've configured MRBS not
|
||||
// to do this (to avoid the email being rejected as spam), in which case we'll
|
||||
// put the user's address on the Cc line, which will enable the recipient to
|
||||
// use it in a reply.
|
||||
if (in_array($action, array('more_info', 'remind')))
|
||||
{
|
||||
if ($mail_settings['use_from_for_all_mail'])
|
||||
{
|
||||
$cc[] = $mrbs_user->mailbox();
|
||||
}
|
||||
else
|
||||
{
|
||||
$from = $mrbs_user->mailbox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($from))
|
||||
{
|
||||
$from = (isset($mail_settings['from'])) ? $mail_settings['from'] : null;
|
||||
}
|
||||
|
||||
// if we're requiring bookings to be approved and this user needs approval
|
||||
// for this room, then get the email addresses of the approvers
|
||||
if (!in_array($action, array('delete', 'reject')) &&
|
||||
$approval_enabled &&
|
||||
!is_book_admin($data['room_id']))
|
||||
{
|
||||
$email = get_approvers_email($data['room_id']);
|
||||
if (!empty($email))
|
||||
{
|
||||
$to = address_merge($to, $email);
|
||||
}
|
||||
}
|
||||
|
||||
$to = address_merge($to, get_admin_email());
|
||||
|
||||
if ($mail_settings['area_admin_on_bookings'])
|
||||
{
|
||||
$to = address_merge($to, get_area_admin_email($data));
|
||||
if (!empty($previous))
|
||||
{
|
||||
$to = address_merge($to, get_area_admin_email($previous));
|
||||
}
|
||||
}
|
||||
|
||||
if ($mail_settings['room_admin_on_bookings'])
|
||||
{
|
||||
$to = address_merge($to, get_room_admin_email($data));
|
||||
if (!empty($previous))
|
||||
{
|
||||
$to = address_merge($to, get_room_admin_email($previous));
|
||||
}
|
||||
}
|
||||
|
||||
if ($mail_settings['booker'])
|
||||
{
|
||||
if (in_array($action, array("approve", "more_info", "reject")))
|
||||
{
|
||||
// Put the addresses on the cc line and the booker will go
|
||||
// on the to line
|
||||
$cc = array_merge($cc, $to);
|
||||
$to = array();
|
||||
}
|
||||
$booker = auth()->getUser($data['create_by']);
|
||||
if (!empty($booker->email))
|
||||
{
|
||||
$to[] = $booker->mailbox();
|
||||
}
|
||||
}
|
||||
|
||||
$addresses = array();
|
||||
$addresses['from'] = $from;
|
||||
$addresses['reply_to'] = (empty($reply_to)) ? '' : get_address_list($reply_to);
|
||||
$addresses['to'] = (empty($to)) ? '' : get_address_list($to);
|
||||
$addresses['cc'] = (empty($cc)) ? '' : get_address_list($cc);
|
||||
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate an email notification.
|
||||
*
|
||||
* @param array $data The booking data
|
||||
* @param array $previous The previous booking data, or an empty array if none
|
||||
* @param bool $is_series Whether this is a series or not
|
||||
* @param string $action The booking action (ie 'approve', 'book', 'delete', 'more_info', 'reject' or 'remind')
|
||||
* @param int[] $start_times An array of start times that have been made or deleted
|
||||
* @param string|null $note A note that is used with 'more_info' and 'reject'
|
||||
*/
|
||||
function notify_by_email(array $data, array $previous, bool $is_series, string $action, array $start_times, ?string $note=null) : void
|
||||
{
|
||||
global $mail_settings, $enable_periods, $mrbs_company;
|
||||
global $timezone;
|
||||
|
||||
if (!in_array($action, ['approve', 'book', 'delete', 'more_info', 'reject', 'remind']))
|
||||
{
|
||||
throw new InvalidArgumentException("Invalid action: $action");
|
||||
}
|
||||
|
||||
mail_debug("Preparing email; action = '" . $action . "'");
|
||||
|
||||
if (in_array($action, ['delete', 'reject']))
|
||||
{
|
||||
// As we are going to cancel this booking, we need to increment the iCalendar sequence number
|
||||
$data['ical_sequence']++;
|
||||
}
|
||||
|
||||
// Set up the addresses (from, to and cc)
|
||||
$addresses = create_addresses($data, $previous, $action);
|
||||
if (empty($addresses['to']) && empty($addresses['cc']))
|
||||
{
|
||||
mail_debug('Email abandoned: no addresses.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up the subject
|
||||
//
|
||||
// If we're sending iCalendar notifications, then it seems that some calendar
|
||||
// applications use the email subject as the booking title instead of the iCal
|
||||
// SUMMARY field. This seems to be wrong, but as a circumvention we'll put the
|
||||
// booking title in the email subject line. (See also SF Tracker id 3297799.)
|
||||
if ($mail_settings['icalendar'] && !$enable_periods)
|
||||
{
|
||||
$subject = $data['name'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$tags = [
|
||||
'approve' => 'mail_subject_approved',
|
||||
'book' => (empty($previous)) ? 'mail_subject_new_entry' : 'mail_subject_changed_entry',
|
||||
'delete' => 'mail_subject_delete',
|
||||
'more_info' => 'mail_subject_more_info',
|
||||
'reject' => 'mail_subject_rejected',
|
||||
'remind' => 'mail_subject_reminder'
|
||||
];
|
||||
$subject = get_mail_vocab($tags[$action], $mrbs_company);
|
||||
}
|
||||
|
||||
// Create the text body
|
||||
$text_body = create_body($data, $previous, $is_series, $action, $start_times, false, $note);
|
||||
|
||||
// Create the HTML body
|
||||
if ($mail_settings['html'])
|
||||
{
|
||||
$html_body = create_body($data, $previous, $is_series, $action, $start_times, true, $note);
|
||||
}
|
||||
|
||||
// Create the iCalendar if required.
|
||||
// Don't add an iCalendar if this is a reminder or a request for more info because then
|
||||
// the recipient probably won't be able to reply to the email: we just want an ordinary
|
||||
// email and not a calendar notification.
|
||||
if ($mail_settings['icalendar'] && !in_array($action, ['more_info', 'remind']))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create the iCalendar.
|
||||
$method = (in_array($action, ['delete', 'reject'])) ? 'CANCEL' : 'REQUEST';
|
||||
$calendar = new Calendar($method);
|
||||
// Add in the VTIMEZONE component if we can.
|
||||
if (false !== ($vtimezone = Timezone::createFromTimezoneName($timezone)))
|
||||
{
|
||||
$tzid = $timezone;
|
||||
$calendar->addComponent($vtimezone);
|
||||
}
|
||||
$events = Event::createFromData($method, $data, $tzid ?? null, $addresses, $is_series, true);
|
||||
}
|
||||
catch (CalendarException $e)
|
||||
{
|
||||
// Don't do anything. We're not able to create an iCalendar, probably because we are using
|
||||
// periods and the times for periods haven't been defined.
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no iCalendar, then it's easy: we just add the email to the queue.
|
||||
if (empty($events))
|
||||
{
|
||||
MailQueue::add(
|
||||
$addresses,
|
||||
$subject,
|
||||
$text_body,
|
||||
$html_body ?? null,
|
||||
null,
|
||||
Language::MAIL_CHARSET
|
||||
);
|
||||
}
|
||||
// Otherwise we need to send separate emails for each event. That's because most email clients
|
||||
// will only automatically add to a calendar the first event in the iCalendar file.
|
||||
else
|
||||
{
|
||||
foreach ($events as $event)
|
||||
{
|
||||
$this_calendar = clone $calendar;
|
||||
// Add in the VEVENT component.
|
||||
// TODO: the addresses have by this stage been MIME-encoded. It would probably be better to
|
||||
// TODO: pass unencoded addresses to createFromData() so that we don't have to decode them,
|
||||
// TODO: as mb_decode_mimeheader() isn't guaranteed to exist.
|
||||
$this_calendar->addComponent($event);
|
||||
$attachment = [
|
||||
'content' => $this_calendar->toString(),
|
||||
'name' => $mail_settings['ics_filename'] . ".ics",
|
||||
'method' => $method
|
||||
];
|
||||
MailQueue::add(
|
||||
$addresses,
|
||||
$subject,
|
||||
$text_body,
|
||||
$html_body ?? null,
|
||||
$attachment,
|
||||
Language::MAIL_CHARSET
|
||||
);
|
||||
unset($this_calendar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function debug_output(string $message) : void
|
||||
{
|
||||
global $mail_settings;
|
||||
|
||||
if (isset($mail_settings['debug_output']) &&
|
||||
($mail_settings['debug_output'] == 'browser'))
|
||||
{
|
||||
echo escape_html($message) . "<br>\n";
|
||||
// flush in case they have output_buffering configured on
|
||||
if (ob_get_length() !== FALSE)
|
||||
{
|
||||
ob_flush();
|
||||
}
|
||||
flush();
|
||||
}
|
||||
else // anything else goes to the error log
|
||||
{
|
||||
error_log($message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mail_debug(string $message) : void
|
||||
{
|
||||
global $mail_settings;
|
||||
|
||||
if ($mail_settings['debug'])
|
||||
{
|
||||
debug_output('[DEBUG] ' . $message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user