feat(notifications): add notify script for backup and restore on plugin installation

This commit is contained in:
Ajit Mehrotra
2025-12-12 17:09:14 -05:00
parent 2455f3a56d
commit 070c44f75f
2 changed files with 334 additions and 0 deletions

View File

@@ -181,6 +181,7 @@ echo "Backing up original files..."
# Define files to backup in a shell variable
FILES_TO_BACKUP=(
"/usr/local/emhttp/plugins/dynamix/scripts/notify"
"/usr/local/emhttp/plugins/dynamix/DisplaySettings.page"
"/usr/local/emhttp/plugins/dynamix/Registration.page"
"/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php"
@@ -324,6 +325,7 @@ exit 0
# Define files to restore in a shell variable - must match backup list
FILES_TO_RESTORE=(
"/usr/local/emhttp/plugins/dynamix/scripts/notify"
"/usr/local/emhttp/plugins/dynamix/DisplaySettings.page"
"/usr/local/emhttp/plugins/dynamix/Registration.page"
"/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php"

View File

@@ -0,0 +1,332 @@
#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
* Copyright 2012, Andrew Hamer-Adams, http://www.pixeleyes.co.nz.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/webGui/include/Encryption.php";
function usage() {
echo <<<EOT
notify [-e "event"] [-s "subject"] [-d "description"] [-i "normal|warning|alert"] [-m "message"] [-x] [-t] [-b] [add]
create a notification
use -e to specify the event
use -s to specify a subject
use -d to specify a short description
use -i to specify the severity
use -m to specify a message (long description)
use -l to specify a link (clicking the notification will take you to that location)
use -x to create a single notification ticket
use -r to specify recipients and not use default
use -t to force send email only (for testing)
use -b to NOT send a browser notification
use -u to specify a custom filename (API use only)
all options are optional
notify init
Initialize the notification subsystem.
notify smtp-init
Initialize sendmail configuration (ssmtp in our case).
notify get
Output a json-encoded list of all the unread notifications.
notify archive file
Move file from 'unread' state to 'archive' state.
EOT;
return 1;
}
function generate_email($event, $subject, $description, $importance, $message, $recipients, $fqdnlink) {
global $ssmtp;
$rcpt = $ssmtp['RcptTo'];
if (!$recipients)
$to = implode(',', explode(' ', trim($rcpt)));
else
$to = $recipients;
if (empty($to)) return;
$subj = "{$ssmtp['Subject']}$subject";
$headers = [];
$headers[] = "MIME-Version: 1.0";
$headers[] = "X-Mailer: PHP/".phpversion();
$headers[] = "Content-type: text/plain; charset=utf-8";
$headers[] = "From: {$ssmtp['root']}";
$headers[] = "Reply-To: {$ssmtp['root']}";
if (($importance == "warning" || $importance == "alert") && $ssmtp['SetEmailPriority']=="True") {
$headers[] = "X-Priority: 1 (highest)";
$headers[] = "X-Mms-Priority: High";
}
$headers[] = "";
$body = [];
if (!empty($fqdnlink)) {
$body[] = "Link: $fqdnlink";
$body[] = "";
}
$body[] = "Event: $event";
$body[] = "Subject: $subject";
$body[] = "Description: $description";
$body[] = "Importance: $importance";
if (!empty($message)) {
$body[] = "";
foreach (explode('\n',$message) as $line)
$body[] = $line;
}
$body[] = "";
return mail($to, $subj, implode("\n", $body), implode("\n", $headers));
}
function safe_filename($string, $maxLength=255) {
$special_chars = ["?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}"];
$string = trim(str_replace($special_chars, "", $string));
$string = preg_replace('~[^0-9a-z _\-]~i', '', $string);
$string = preg_replace('~[- ]~i', '_', $string);
// limit filename length to $maxLength characters
return substr(trim($string), 0, $maxLength);
}
/*
Call this when using the subject field in email or agents. Do not use when showing the subject in a browser.
Removes all HTML entities from subject line, is specifically targetting the my_temp() function, which adds '&#8201;&#176;'
*/
function clean_subject($subject) {
$subject = preg_replace("/&#?[a-z0-9]{2,8};/i"," ",$subject);
return $subject;
}
/**
* Wrap string values in double quotes for INI compatibility and escape quotes/backslashes.
* Numeric types remain unquoted so they can be parsed as-is.
*/
function ini_encode_value($value) {
if (is_int($value) || is_float($value)) return $value;
if (is_bool($value)) return $value ? 'true' : 'false';
$value = (string)$value;
return '"'.strtr($value, ["\\"=>"\\\\", '"' => '\\"']).'"';
}
function build_ini_string(array $data) {
$lines = [];
foreach ($data as $key => $value) {
$lines[] = "{$key}=".ini_encode_value($value);
}
return implode("\n", $lines)."\n";
}
/**
* Trims and unescapes strings (eg quotes, backslashes) if necessary.
*/
function ini_decode_value($value) {
$value = trim($value);
$length = strlen($value);
if ($length >= 2 && $value[0] === '"' && $value[$length-1] === '"') {
return stripslashes(substr($value, 1, -1));
}
return $value;
}
// start
if ($argc == 1) exit(usage());
extract(parse_plugin_cfg("dynamix",true));
$path = _var($notify,'path','/tmp/notifications');
$unread = "$path/unread";
$archive = "$path/archive";
$agents_dir = "/boot/config/plugins/dynamix/notifications/agents";
if (is_dir($agents_dir)) {
$agents = [];
foreach (array_diff(scandir($agents_dir), ['.','..']) as $p) {
if (file_exists("{$agents_dir}/{$p}")) $agents[] = "{$agents_dir}/{$p}";
}
} else {
$agents = NULL;
}
switch ($argv[1][0]=='-' ? 'add' : $argv[1]) {
case 'init':
$files = glob("$unread/*.notify", GLOB_NOSORT);
foreach ($files as $file) if (!is_readable($file)) chmod($file,0666);
break;
case 'smtp-init':
@mkdir($unread,0755,true);
@mkdir($archive,0755,true);
$conf = [];
$conf[] = "# Generated settings:";
$conf[] = "Root={$ssmtp['root']}";
$domain = strtok($ssmtp['root'],'@');
$domain = strtok('@');
$conf[] = "rewriteDomain=$domain";
$conf[] = "FromLineOverride=YES";
$conf[] = "Mailhub={$ssmtp['server']}:{$ssmtp['port']}";
$conf[] = "UseTLS={$ssmtp['UseTLS']}";
$conf[] = "UseSTARTTLS={$ssmtp['UseSTARTTLS']}";
if ($ssmtp['AuthMethod'] != "none") {
$conf[] = "AuthMethod={$ssmtp['AuthMethod']}";
$conf[] = "AuthUser={$ssmtp['AuthUser']}";
$conf[] = "AuthPass=".base64_decrypt($ssmtp['AuthPass']);
}
$conf[] = "";
file_put_contents("/etc/ssmtp/ssmtp.conf", implode("\n", $conf));
break;
case 'cron-init':
@mkdir($unread,0755,true);
@mkdir($archive,0755,true);
$text = empty($notify['status']) ? "" : "# Generated array status check schedule:\n{$notify['status']} $docroot/plugins/dynamix/scripts/statuscheck &> /dev/null\n\n";
parse_cron_cfg("dynamix", "status-check", $text);
$text = empty($notify['unraidos']) ? "" : "# Generated Unraid OS update check schedule:\n{$notify['unraidos']} $docroot/plugins/dynamix.plugin.manager/scripts/unraidcheck &> /dev/null\n\n";
parse_cron_cfg("dynamix", "unraid-check", $text);
$text = empty($notify['version']) ? "" : "# Generated plugins version check schedule:\n{$notify['version']} $docroot/plugins/dynamix.plugin.manager/scripts/plugincheck &> /dev/null\n\n";
parse_cron_cfg("dynamix", "plugin-check", $text);
$text = empty($notify['system']) ? "" : "# Generated system monitoring schedule:\n{$notify['system']} $docroot/plugins/dynamix/scripts/monitor &> /dev/null\n\n";
parse_cron_cfg("dynamix", "monitor", $text);
$text = empty($notify['docker_update']) ? "" : "# Generated docker monitoring schedule:\n{$notify['docker_update']} $docroot/plugins/dynamix.docker.manager/scripts/dockerupdate check &> /dev/null\n\n";
parse_cron_cfg("dynamix", "docker-update", $text);
$text = empty($notify['language_update']) ? "" : "# Generated languages version check schedule:\n{$notify['language_update']} $docroot/plugins/dynamix.plugin.manager/scripts/languagecheck &> /dev/null\n\n";
parse_cron_cfg("dynamix", "language-check", $text);
break;
case 'add':
$event = 'Unraid Status';
$subject = 'Notification';
$description = 'No description';
$importance = 'normal';
$message = $recipients = $link = $fqdnlink = '';
$timestamp = time();
$ticket = $timestamp;
$mailtest = false;
$overrule = false;
$noBrowser = false;
$customFilename = false;
$options = getopt("l:e:s:d:i:m:r:u:xtb");
foreach ($options as $option => $value) {
switch ($option) {
case 'e':
$event = $value;
break;
case 's':
$subject = $value;
break;
case 'd':
$description = $value;
break;
case 'i':
$importance = strtok($value,' ');
$overrule = strtok(' ');
break;
case 'm':
$message = $value;
break;
case 'r':
$recipients = $value;
break;
case 'x':
$ticket = 'ticket';
break;
case 't':
$mailtest = true;
break;
case 'b':
$noBrowser = true;
break;
case 'l':
$nginx = (array)@parse_ini_file('/var/local/emhttp/nginx.ini');
$link = $value;
$fqdnlink = (strpos($link,"http") === 0) ? $link : ($nginx['NGINX_DEFAULTURL']??'').$link;
break;
case 'u':
$customFilename = $value;
break;
}
}
if ($customFilename) {
$filename = safe_filename($customFilename);
} else {
// suffix length: _{timestamp}.notify = 1+10+7 = 18 chars.
$suffix = "_{$ticket}.notify";
$max_name_len = 255 - strlen($suffix);
// sanitize event, truncating it to leave room for suffix
$clean_name = safe_filename($event, $max_name_len);
// construct filename with suffix (underscore separator matches safe_filename behavior)
$filename = "{$clean_name}{$suffix}";
}
$unread = "{$unread}/{$filename}";
$archive = "{$archive}/{$filename}";
if (file_exists($archive)) break;
$entity = $overrule===false ? $notify[$importance] : $overrule;
$cleanSubject = clean_subject($subject);
$archiveData = [
'timestamp' => $timestamp,
'event' => $event,
'subject' => $cleanSubject,
'description' => $description,
'importance' => $importance,
];
if ($message) $archiveData['message'] = str_replace('\n','<br>',$message);
if (!$mailtest) file_put_contents($archive, build_ini_string($archiveData));
if (($entity & 1)==1 && !$mailtest && !$noBrowser) {
$unreadData = [
'timestamp' => $timestamp,
'event' => $event,
'subject' => $cleanSubject,
'description' => $description,
'importance' => $importance,
'link' => $link,
];
file_put_contents($unread, build_ini_string($unreadData));
}
if (($entity & 2)==2 || $mailtest) generate_email($event, $cleanSubject, str_replace('<br>','. ',$description), $importance, $message, $recipients, $fqdnlink);
if (($entity & 4)==4 && !$mailtest) { if (is_array($agents)) {foreach ($agents as $agent) {exec("TIMESTAMP='$timestamp' EVENT=".escapeshellarg($event)." SUBJECT=".escapeshellarg($cleanSubject)." DESCRIPTION=".escapeshellarg($description)." IMPORTANCE=".escapeshellarg($importance)." CONTENT=".escapeshellarg($message)." LINK=".escapeshellarg($fqdnlink)." bash ".$agent);};}};
break;
case 'get':
$output = [];
$json = [];
$files = glob("$unread/*.notify", GLOB_NOSORT);
usort($files, function($a,$b){return filemtime($a)-filemtime($b);});
$i = 0;
foreach ($files as $file) {
$fields = file($file,FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
$time = true;
$output[$i]['file'] = basename($file);
$output[$i]['show'] = (fileperms($file) & 0x0FFF)==0400 ? 0 : 1;
foreach ($fields as $field) {
if (!$field) continue;
# limit the explode('=', …) used during reads to two pieces so values containing = remain intact
[$key,$val] = array_pad(explode('=', $field, 2),2,'');
if ($time) {$val = date($notify['date'].' '.$notify['time'], $val); $time = false;}
# unescape the value before emitting JSON, so the browser UI
# and any scripts calling `notify get` still see plain strings
$output[$i][trim($key)] = ini_decode_value($val);
}
$i++;
}
echo json_encode($output, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
break;
case 'archive':
if ($argc != 3) exit(usage());
$file = $argv[2];
if (strpos(realpath("$unread/$file"),$unread.'/')===0) @unlink("$unread/$file");
break;
}
exit(0);
?>