<?php
abstract class NotificationTransport
{
public static function getInstance(array $data)
{
switch ($data['type'] ?? '') {
case 'mail':
return new MailNotificationTransport($data);
case 'irc':
return new IrcNotificationTransport($data);
case 'http':
return new HttpNotificationTransport($data);
case 'group':
return new GroupNotificationTransport($data);
}
error_log('Invalid Notification Transport: ' . ($data['type'] ?? '(unset)'));
return null;
}
public static function newGroup(int ...$ids): GroupNotificationTransport
{
return new GroupNotificationTransport(['group-list' => $ids]);
}
public abstract function __construct(array $data);
public abstract function toString(): string;
public abstract function fire(string $subject, string $message, array $raw): bool;
public abstract function isValid(): bool;
}
class MailNotificationTransport extends NotificationTransport
{
/** @var int */
private $mailConfigId;
/** @var int[] */
private $userIds;
/** @var string */
private $extraMails;
public function __construct(array $data)
{
$this->mailConfigId = (int)($data['mail-config-id'] ?? 0);
$this->userIds = array_map(function ($i) { return (int)$i; }, $data['mail-users'] ?? []);
$this->extraMails = (string)($data['mail-extra-mails'] ?? '');
}
public function toString(): string
{
static $mailList = null;
if ($mailList === null) {
$mailList = Database::queryIndexedList("SELECT configid, host, senderaddress, replyto
FROM mail_config");
}
$str = 'Via: ' . ($mailList[$this->mailConfigId]['host'] ?? '<none>')
. ' as ' . ($mailList[$this->mailConfigId]['senderaddress'] ?? $mailList[$this->mailConfigId]['replyto'] ?? '<none>');
if (!empty($this->userIds)) {
$str .= ', Users: ' . count($this->userIds);
}
if (!empty($this->extraMails)) {
$str .= ', External: ' . substr_count($this->extraMails, '@');
}
return $str;
}
public function fire(string $subject, string $message, array $raw): bool
{
if (!$this->isValid())
return false;
$addrsOut = [];
if (preg_match_all('/[^@\s]+@[^@\s]+/', $this->extraMails, $out)) {
$addrsOut = $out[0];
}
if (!empty($this->userIds)) {
$mails = Database::queryColumnArray("SELECT email
FROM user
WHERE userid IN (:users)",
['users' => $this->userIds]);
foreach ($mails as $mail) {
if (preg_match('/^[^@\s]+@[^@\s]+$/', $mail)) {
$addrsOut[] = $mail;
}
}
}
if (empty($addrsOut))
return false;
Mailer::queue($this->mailConfigId, $addrsOut, $subject, $message);
return true;
}
public function isValid(): bool
{
if ($this->mailConfigId === 0)
return false;
$mailer = Mailer::instanceFromConfig($this->mailConfigId);
return $mailer !== null;
}
}
class IrcNotificationTransport extends NotificationTransport
{
private $server;
private $serverPasswort;
private $target;
private $nickName;
public function __construct(array $data)
{
$this->server = $data['irc-server'] ?? '';
$this->serverPasswort = $data['irc-server-password'] ?? '';
$this->target = $data['irc-target'] ?? '';
$this->nickName = $data['irc-nickname'] ?? 'BWLP-' . mt_rand(10000, 99999);
}
public function toString(): string
{
return '(' . $this->server . '), ' . $this->nickName . ' @ ' . $this->target;
}
public function fire(string $subject, string $message, array $raw): bool
{
if (!$this->isValid())
return false;
return !Taskmanager::isFailed(Taskmanager::submit('IrcNotification', [
'serverAddress' => $this->server,
'serverPassword' => $this->serverPasswort,
'channel' => $this->target,
'message' => preg_replace('/[\r\n]+\s*/', ' ', $message),
'nickName' => $this->nickName,
]));
}
public function isValid(): bool
{
return !empty($this->server) && !empty($this->target);
}
}
class HttpNotificationTransport extends NotificationTransport
{
/** @var string */
private $uri;
/** @var string */
private $method;
/** @var string */
private $postField;
/** @var string */
private $postFormat;
public function __construct(array $data)
{
$this->uri = $data['http-uri'] ?? '';
$this->method = $data['http-method'] ?? 'POST';
$this->postField = $data['http-post-field'] ?? 'message=%TEXT%&subject=%SUBJECT%';
$this->postFormat = $data['http-post-format'] ?? 'FORM';
}
public function toString(): string
{
return $this->uri . ' (' . $this->method . ')';
}
public function fire(string $subject, string $message, array $raw): bool
{
if (!$this->isValid())
return false;
$url = str_replace(['%TEXT%', '%SUBJECT%'], [urlencode($message), urlencode($subject)], $this->uri);
if ($this->method === 'POST') {
switch ($this->postFormat) {
case 'FORM':
$body = str_replace(['%TEXT%', '%SUBJECT%'], [urlencode($message), urlencode($subject)], $this->postField);
$ctype = 'application/x-www-form-urlencoded';
break;
case 'JSON':
$body = str_replace(['%TEXT%', '%SUBJECT%'], [json_encode($message),
json_encode($subject)], $this->postField);
$ctype = 'application/json';
break;
default:
$out = [];
foreach ($raw as $k1 => $a) {
foreach ($a as $k2 => $v) {
$out["$k1.$k2"] = $v;
}
}
$body = json_encode($out);
$ctype = 'application/json';
}
} else {
$body = null;
$ctype = null;
}
return !Taskmanager::isFailed(Taskmanager::submit('HttpRequest', [
'url' => $url,
'postData' => $body,
'contentType' => $ctype,
]));
}
public function isValid(): bool
{
return !empty($this->uri);
}
}
class GroupNotificationTransport extends NotificationTransport
{
/** @var int[] list of contained notification transports */
private $list;
public function __construct(array $data)
{
$this->list = array_map(function ($i) { return (int)$i; }, $data['group-list'] ?? []);
}
public function toString(): string
{
static $groupList = null;
if ($groupList === null) {
$groupList = Database::queryKeyValueList("SELECT transportid, title FROM notification_backend");
}
$out = array_map(function ($i) use ($groupList) { return $groupList[$i] ?? "#$i"; }, $this->list);
return implode(', ', $out);
}
public function fire(string $subject, string $message, array $raw): bool
{
// This is static, so recursing into groups will keep track of ones we already saw
static $done = false;
$first = ($done === false);
if ($first) { // Non-recursive call, init list
$done = [];
}
$list = array_diff($this->list, $done);
if (!empty($list)) {
$done = array_merge($done, $list);
$res = Database::simpleQuery("SELECT data FROM notification_backend WHERE transportid IN (:ids)",
['ids' => $list]);
foreach ($res as $row) {
$data = json_decode($row['data'], true);
if (is_array($data)) {
$inst = NotificationTransport::getInstance($data);
if ($inst !== null) {
$inst->fire($subject, $message, $raw);
}
}
}
}
if ($first) {
$done = false; // Outer-most call, reset
}
return true;
}
public function isValid(): bool
{
// Do we really care about empty groups? They might be pointless, but not really invalid
// We could consider groups containing invalid IDs as invalid, but that would mean that we
// potentially ignore all the other existing IDs in this group, as it would never fire
return true;
}
}