<?php if (!defined('PmWiki'))exit; /** Bulletin: Recent changes notifications for PmWiki Written by (c) 2025 Petko Yotov www.pmwiki.org/petko This text is written for PmWiki; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. See pmwiki.php for full details and lack of warranty. Copyright 2025 Petko Yotov www.pmwiki.org/petko */ $RecipeInfo['Bulletin']['Version'] = '2025-04-10'; include_once("$FarmD/scripts/trails.php"); include_once("$FarmD/scripts/tools.php"); SDVA($Bulletin, [ 'subject' => "[$WikiTitle] Bulletin", 'defaultoptions' => 'delay=10m squelch=3h fmt=#titlesummary order=time', 'recipientspage' => '$SiteAdminGroup.BulletinList', 'sentfile' => "$WorkDir/.sentbulletins", 'trailpage' => '$SiteGroup.AllRecentChanges', 'maxextime' => 10, 'renderfunction' => 'FmtBulletin', 'authfunction' => false, # BulletinAuthPage 'shouldrunfunction' => false, 'mailfunction' => null, 'attachments' => [], 'headers' => [], ]); register_shutdown_function('BulletinUpdate', $pagename, getcwd()); function BulletinAuthPage($pagename, $level, $authprompt=true, $since=0) { return ReadPage($pagename, $since); } function BulletinUpdate($pagename, $dir='') { global $Bulletin, $B3, $Now, $FmtPV, $EnableRedirect, $PCache; $EnableRedirect = false; if ($dir) { flush(); chdir($dir); } $srfn = $Bulletin['shouldrunfunction']; if ($srfn && function_exists($srfn) && !$srfn($pagename)) return; # used to indicate new pages SDV($FmtPV['$BulPubTime'], '$page["ctime"]??null'); if(isset($B3)) # B3 compatibility $FmtPV['$B3Intro'] = 'B3Intro($pn, "ALWAYS")'; $abort = ignore_user_abort(true); $endtime = $Now + $Bulletin['maxextime']; $nfile = FmtPageName($Bulletin['sentfile'], $pagename); if(!file_exists($nfile)) touch($nfile); $nfp = @fopen($nfile, 'c+'); if(!$nfp) return; # misconfiguration, file permissions? if(!flock($nfp, LOCK_EX | LOCK_NB, $wouldblock)) { if ($wouldblock) return; # another process is currently working } clearstatcache(); $fc = stream_get_contents($nfp); $sendtimes = $osendtimes = $fc? unserialize(rtrim($fc)) : []; $defopts = ParseArgs($Bulletin['defaultoptions']); unset($defopts[''], $defopts['#']); $defopts['subject'] = $Bulletin['subject']; $bpn = FmtPageName($Bulletin['recipientspage'], $pagename); $bpage = ReadPage($bpn, READPAGE_CURRENT); $btext = $bpage['text'] ?? ''; if(!$btext) return; if(!preg_match_all('!^ *bulletin=.*$!m', $btext, $blines)) return; $oldeststamp = $Now+86400; $recipients = []; foreach($blines[0] as $line) { $lopts = ParseArgs($line); unset($lopts[''], $lopts['#']); $opts = array_merge($defopts, $lopts); $opts = PPTD($opts); $anchor = $opts['bulletin']; unset($opts['bulletin']); $pla = ''; foreach($opts as $k=>$v) if(!preg_match('/^(delay|squelch|subject)$/', $k)) $pla .= " $k=\"$v\""; $opts['pla'] = $pla; if(!preg_match("/\\[\\[ *$anchor *\\]\\](.*?)\\[\\[ *#/s", $btext, $section)) continue; preg_match_all( "/[a-zA-Z0-9._%+'-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/", $section[1], $matches ); foreach($matches[0] as $email) { $recipients[$email] = $opts; if(empty($sendtimes[$email])) $sendtimes[$email] = -$Now; $recipients[$email]['lastnotified'] = $sendtimes[$email]; $oldeststamp = min($oldeststamp, abs($sendtimes[$email])); } } $tpn = FmtPageName($Bulletin['trailpage'], $pagename); $trail = ReadTrail($pagename, $tpn); $pages = []; foreach($trail as $a) { $pn = $a['pagename']; if(!PageExists($pn)) continue; # deleted page $mtime = PageVar($pn, '$LastModifiedTime'); $ctime = PageVar($pn, '$BulPubTime'); if(($ctime && $ctime>$Now)|| $mtime>$Now) continue; # unpublished page if($mtime<$oldeststamp) break; # pages too old, already notified $pages[$pn] = ['ctime'=>$ctime, 'mtime'=>$mtime]; } $newsendtimes = []; foreach($recipients as $email=>$eopts) { if(time()<=$endtime) { $not = BulletinNotifyOne($pagename, $email, $eopts, $pages); if($not) $recipients[$email]['lastnotified'] = $not; } $newsendtimes[$email] = $recipients[$email]['lastnotified']; } asort($newsendtimes); if($newsendtimes != $osendtimes) { ftruncate($nfp, 0); rewind($nfp); fwrite($nfp, serialize($newsendtimes) . "\n"); } flock($nfp, LOCK_UN); fclose($nfp); fixperms($nfile); } function BulletinNotifyOne($pagename, $email, $opts, $pages){ global $Now, $Bulletin, $MailFunction; $prevnotified = $opts['lastnotified']; $lastnotif = abs($prevnotified); $nextnotif = $prevnotified<0 ? $lastnotif : $lastnotif+$opts['squelch']; if($nextnotif>$Now) return $prevnotified; $list = $filter = []; if (@$opts['group']) $filter[] = FixGlob($opts['group'], '$1$2.*'); if (@$opts['name']) $filter[] = FixGlob($opts['name'], '$1*.$2'); foreach($pages as $pn=>$a) { if($a['mtime']<$lastnotif) continue; if(!MatchPageNames($pn, $filter)) continue; $list[$pn] = $a['mtime']; } if(!$list) return $prevnotified; $oldeststamp = min($list); if($oldeststamp>$Now-$opts['delay']) return $prevnotified; $rfn = $Bulletin['renderfunction']; $parts = $rfn($pagename, $email, $opts, $list); if($Bulletin['attachments']) $parts = array_merge((array)$parts, (array)$Bulletin['attachments']); list($body, $headers) = PmMultipartMail($parts, $pagename); $headers = array_merge($headers, $Bulletin['headers']); $mf = IsEnabled($Bulletin['mailfunction'], IsEnabled($MailFunction, 'mail')); $sent = $mf($email, $opts['subject'], $body, $headers); return $sent? time() : $prevnotified; } function FmtBulletin($pagename, $email, $opts, $pagelist) { global $SearchPatterns; $SearchPatterns['bulletin'] = implode(',',array_keys($pagelist)); return "markup:(:pagelist list=bulletin {$opts['pla']}:)\n"; }