$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'] ?? '') . ' as ' . ($mailList[$this->mailConfigId]['senderaddress'] ?? $mailList[$this->mailConfigId]['replyto'] ?? ''); 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; } }