1<?php 2 3namespace dokuwiki\Subscriptions; 4 5use dokuwiki\ChangeLog\PageChangeLog; 6use dokuwiki\Extension\AuthPlugin; 7use dokuwiki\Input\Input; 8use Exception; 9 10class BulkSubscriptionSender extends SubscriptionSender 11{ 12 /** 13 * Send digest and list subscriptions 14 * 15 * This sends mails to all subscribers that have a subscription for namespaces above 16 * the given page if the needed $conf['subscribe_time'] has passed already. 17 * 18 * This function is called form lib/exe/indexer.php 19 * 20 * @param string $page 21 * @return int number of sent mails 22 * @throws Exception 23 */ 24 public function sendBulk($page) 25 { 26 $subscriberManager = new SubscriberManager(); 27 if (!$subscriberManager->isenabled()) { 28 return 0; 29 } 30 31 /** @var AuthPlugin $auth */ 32 global $auth; 33 global $conf; 34 global $USERINFO; 35 /** @var Input $INPUT */ 36 global $INPUT; 37 $count = 0; 38 39 $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']); 40 41 // remember current user info 42 $olduinfo = $USERINFO; 43 $olduser = $INPUT->server->str('REMOTE_USER'); 44 45 foreach ($subscriptions as $target => $users) { 46 if (!$this->lock($target)) { 47 continue; 48 } 49 50 foreach ($users as $user => $info) { 51 [$style, $lastupdate] = $info; 52 53 $lastupdate = (int)$lastupdate; 54 if ($lastupdate + $conf['subscribe_time'] > time()) { 55 // Less than the configured time period passed since last 56 // update. 57 continue; 58 } 59 60 // Work as the user to make sure ACLs apply correctly 61 $USERINFO = $auth->getUserData($user); 62 $INPUT->server->set('REMOTE_USER', $user); 63 if ($USERINFO === false) { 64 continue; 65 } 66 if (!$USERINFO['mail']) { 67 continue; 68 } 69 70 if (str_ends_with($target, ':')) { 71 // subscription target is a namespace, get all changes within 72 $changes = getRecentsSince($lastupdate, null, getNS($target)); 73 } else { 74 // single page subscription, check ACL ourselves 75 if (auth_quickaclcheck($target) < AUTH_READ) { 76 continue; 77 } 78 $meta = p_get_metadata($target); 79 $changes = [$meta['last_change']]; 80 } 81 82 // Filter out pages only changed in small and own edits 83 $change_ids = []; 84 foreach ($changes as $rev) { 85 $n = 0; 86 $pagelog = new PageChangeLog($rev['id']); 87 while ( 88 !is_null($rev) && $rev['date'] >= $lastupdate && 89 ($INPUT->server->str('REMOTE_USER') === $rev['user'] || 90 $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) 91 ) { 92 $revisions = $pagelog->getRevisions($n++, 1); 93 $rev = ($revisions !== []) ? $pagelog->getRevisionInfo($revisions[0]) : null; 94 } 95 96 if (!is_null($rev) && $rev['date'] >= $lastupdate) { 97 // Some change was not a minor one and not by myself 98 $change_ids[] = $rev['id']; 99 } 100 } 101 102 // send it 103 if ($style === 'digest') { 104 foreach ($change_ids as $change_id) { 105 $this->sendDigest( 106 $USERINFO['mail'], 107 $change_id, 108 $lastupdate 109 ); 110 $count++; 111 } 112 } elseif ($style === 'list') { 113 $this->sendList($USERINFO['mail'], $change_ids, $target, $lastupdate); 114 $count++; 115 } 116 // TODO: Handle duplicate subscriptions. 117 118 // Update notification time. 119 $subscriberManager->add($target, $user, $style, time()); 120 } 121 $this->unlock($target); 122 } 123 124 // restore current user info 125 $USERINFO = $olduinfo; 126 $INPUT->server->set('REMOTE_USER', $olduser); 127 return $count; 128 } 129 130 /** 131 * Lock subscription info 132 * 133 * We don't use io_lock() her because we do not wait for the lock and use a larger stale time 134 * 135 * @param string $id The target page or namespace, specified by id; Namespaces 136 * are identified by appending a colon. 137 * 138 * @return bool true, if you got a succesful lock 139 * @author Adrian Lang <lang@cosmocode.de> 140 */ 141 protected function lock($id) 142 { 143 global $conf; 144 145 $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; 146 147 if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) { 148 // looks like a stale lock - remove it 149 @rmdir($lock); 150 } 151 152 // try creating the lock directory 153 if (!@mkdir($lock)) { 154 return false; 155 } 156 157 if ($conf['dperm']) { 158 chmod($lock, $conf['dperm']); 159 } 160 return true; 161 } 162 163 /** 164 * Unlock subscription info 165 * 166 * @param string $id The target page or namespace, specified by id; Namespaces 167 * are identified by appending a colon. 168 * 169 * @return bool 170 * @author Adrian Lang <lang@cosmocode.de> 171 */ 172 protected function unlock($id) 173 { 174 global $conf; 175 $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock'; 176 return @rmdir($lock); 177 } 178 179 /** 180 * Return the last revision before lastupdate. 181 * 182 * @param string $id The target page or namespace, specified by id; Namespaces 183 * are identified by appending a colon. 184 * 185 * @return int 186 * @author Michael Stapelberg <stapelberg+dokuwiki@google.com> 187 */ 188 protected function lastRevBefore($id, $lastupdate) 189 { 190 $pagelog = new PageChangeLog($id); 191 $n = 0; 192 do { 193 $rev = $pagelog->getRevisions($n++, 1); 194 $rev = ($rev !== []) ? $rev[0] : null; 195 } while (!is_null($rev) && $rev > $lastupdate); 196 return $rev; 197 } 198 199 /** 200 * Send a digest mail 201 * 202 * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff() 203 * but determines the last known revision first 204 * 205 * @param string $subscriber_mail The target mail address 206 * @param string $id The ID 207 * @param int $lastupdate Time of the last notification 208 * 209 * @return bool 210 * @author Adrian Lang <lang@cosmocode.de> 211 * 212 */ 213 protected function sendDigest($subscriber_mail, $id, $lastupdate) 214 { 215 $rev = $this->lastRevBefore($id, $lastupdate); 216 217 // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better 218 $pageSubSender = new PageSubscriptionSender($this->mailer); 219 return $pageSubSender->sendPageDiff( 220 $subscriber_mail, 221 'subscr_digest', 222 $id, 223 $rev 224 ); 225 } 226 227 /** 228 * Send a list mail 229 * 230 * Sends a list mail showing a list of changed pages. 231 * 232 * @param string $subscriber_mail The target mail address 233 * @param array $ids Array of ids 234 * @param string $ns_id The id of the namespace 235 * @param int $lastupdate Time of the last notification 236 * 237 * @return bool true if a mail was sent 238 * @author Adrian Lang <lang@cosmocode.de> 239 * 240 */ 241 protected function sendList($subscriber_mail, $ids, $ns_id, $lastupdate) 242 { 243 global $lang; 244 245 if ($ids === []) { 246 return false; 247 } 248 249 $tlist = ''; 250 $hlist = '<ul>'; 251 foreach ($ids as $id) { 252 $last = $this->lastRevBefore($id, $lastupdate); 253 $link = wl($id, [], true); 254 $tlist .= '* ' . $link . NL; 255 $hlist .= '<li>'; 256 $hlist .= '<a href="' . $link . '">' . hsc($id) . '</a>'; 257 if (!is_null($last)) { 258 $difflink = wl($id, ['do' => 'diff', 'rev' => $last], true); 259 $hlist .= ' (<a href="' . $difflink . '">' . $lang['diff'] . '</a>)'; 260 } 261 $hlist .= '</li>' . NL; 262 } 263 $hlist .= '</ul>'; 264 265 $id = prettyprint_id($ns_id); 266 $trep = [ 267 'DIFF' => rtrim($tlist), 268 'PAGE' => $id, 269 'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'), 270 ]; 271 $hrep = [ 272 'DIFF' => $hlist, 273 ]; 274 275 return $this->send( 276 $subscriber_mail, 277 'subscribe_list', 278 $ns_id, 279 'subscr_list', 280 $trep, 281 $hrep 282 ); 283 } 284} 285