<?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";
}