1<?php 2 3namespace dokuwiki\Subscriptions; 4 5use dokuwiki\Extension\AuthPlugin; 6use dokuwiki\Input\Input; 7use Exception; 8 9class SubscriberManager 10{ 11 /** 12 * Check if subscription system is enabled 13 * 14 * @return bool 15 */ 16 public function isenabled() 17 { 18 return actionOK('subscribe'); 19 } 20 21 /** 22 * Adds a new subscription for the given page or namespace 23 * 24 * This will automatically overwrite any existent subscription for the given user on this 25 * *exact* page or namespace. It will *not* modify any subscription that may exist in higher namespaces. 26 * 27 * @throws Exception when user or style is empty 28 * 29 * @param string $id The target page or namespace, specified by id; Namespaces 30 * are identified by appending a colon. 31 * @param string $user 32 * @param string $style 33 * @param string $data 34 * 35 * @return bool 36 */ 37 public function add($id, $user, $style, $data = '') 38 { 39 if (!$this->isenabled()) { 40 return false; 41 } 42 43 // delete any existing subscription 44 $this->remove($id, $user); 45 46 $user = auth_nameencode(trim($user)); 47 $style = trim($style); 48 $data = trim($data); 49 50 if (!$user) { 51 throw new Exception('no subscription user given'); 52 } 53 if (!$style) { 54 throw new Exception('no subscription style given'); 55 } 56 if (!$data) { 57 $data = time(); 58 } //always add current time for new subscriptions 59 60 $line = "$user $style $data\n"; 61 $file = $this->file($id); 62 return io_saveFile($file, $line, true); 63 } 64 65 66 /** 67 * Removes a subscription for the given page or namespace 68 * 69 * This removes all subscriptions matching the given criteria on the given page or 70 * namespace. It will *not* modify any subscriptions that may exist in higher 71 * namespaces. 72 * 73 * @param string $id The target object’s (namespace or page) id 74 * @param string|array $user 75 * @param string|array $style 76 * @param string|array $data 77 * 78 * @return bool 79 * @throws Exception 80 */ 81 public function remove($id, $user = null, $style = null, $data = null) 82 { 83 if (!$this->isenabled()) { 84 return false; 85 } 86 87 $file = $this->file($id); 88 if (!file_exists($file)) { 89 return true; 90 } 91 92 $regexBuilder = new SubscriberRegexBuilder(); 93 $re = $regexBuilder->buildRegex($user, $style, $data); 94 return io_deleteFromFile($file, $re, true); 95 } 96 97 /** 98 * Get data for $INFO['subscribed'] 99 * 100 * $INFO['subscribed'] is either false if no subscription for the current page 101 * and user is in effect. Else it contains an array of arrays with the fields 102 * “target”, “style”, and optionally “data”. 103 * 104 * @param string $id Page ID, defaults to global $ID 105 * @param string $user User, defaults to $_SERVER['REMOTE_USER'] 106 * 107 * @return array|false 108 * @throws Exception 109 * 110 * @author Adrian Lang <lang@cosmocode.de> 111 */ 112 public function userSubscription($id = '', $user = '') 113 { 114 if (!$this->isenabled()) { 115 return false; 116 } 117 118 global $ID; 119 /** @var Input $INPUT */ 120 global $INPUT; 121 if (!$id) { 122 $id = $ID; 123 } 124 if (!$user) { 125 $user = $INPUT->server->str('REMOTE_USER'); 126 } 127 128 if (empty($user)) { 129 // not logged in 130 return false; 131 } 132 133 $subs = $this->subscribers($id, $user); 134 if ($subs === []) { 135 return false; 136 } 137 138 $result = []; 139 foreach ($subs as $target => $info) { 140 $result[] = [ 141 'target' => $target, 142 'style' => $info[$user][0], 143 'data' => $info[$user][1], 144 ]; 145 } 146 147 return $result; 148 } 149 150 /** 151 * Recursively search for matching subscriptions 152 * 153 * This function searches all relevant subscription files for a page or 154 * namespace. 155 * 156 * @param string $page The target object’s (namespace or page) id 157 * @param string|array $user 158 * @param string|array $style 159 * @param string|array $data 160 * 161 * @return array 162 * @throws Exception 163 * 164 * @author Adrian Lang <lang@cosmocode.de> 165 * 166 */ 167 public function subscribers($page, $user = null, $style = null, $data = null) 168 { 169 if (!$this->isenabled()) { 170 return []; 171 } 172 173 // Construct list of files which may contain relevant subscriptions. 174 $files = [':' => $this->file(':')]; 175 do { 176 $files[$page] = $this->file($page); 177 $page = getNS(rtrim($page, ':')) . ':'; 178 } while ($page !== ':'); 179 180 $regexBuilder = new SubscriberRegexBuilder(); 181 $re = $regexBuilder->buildRegex($user, $style, $data); 182 183 // Handle files. 184 $result = []; 185 foreach ($files as $target => $file) { 186 if (!file_exists($file)) { 187 continue; 188 } 189 190 $lines = file($file); 191 foreach ($lines as $line) { 192 // fix old style subscription files 193 if (strpos($line, ' ') === false) { 194 $line = trim($line) . " every\n"; 195 } 196 197 // check for matching entries 198 if (!preg_match($re, $line, $m)) { 199 continue; 200 } 201 202 // if no last sent is set, use 0 203 if (!isset($m[3])) { 204 $m[3] = 0; 205 } 206 207 $u = rawurldecode($m[1]); // decode the user name 208 if (!isset($result[$target])) { 209 $result[$target] = []; 210 } 211 $result[$target][$u] = [$m[2], $m[3]]; // add to result 212 } 213 } 214 return array_reverse($result); 215 } 216 217 /** 218 * Default callback for COMMON_NOTIFY_ADDRESSLIST 219 * 220 * Aggregates all email addresses of user who have subscribed the given page with 'every' style 221 * 222 * @param array &$data Containing the entries: 223 * - $id (the page id), 224 * - $self (whether the author should be notified, 225 * - $addresslist (current email address list) 226 * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value) 227 * @throws Exception 228 * 229 * @author Adrian Lang <lang@cosmocode.de> 230 * @author Steven Danz <steven-danz@kc.rr.com> 231 * 232 * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead, 233 * use an array for the addresses within it 234 */ 235 public function notifyAddresses(&$data) 236 { 237 if (!$this->isenabled()) { 238 return; 239 } 240 241 /** @var AuthPlugin $auth */ 242 global $auth; 243 global $conf; 244 /** @var \Input $INPUT */ 245 global $INPUT; 246 247 $id = $data['id']; 248 $self = $data['self']; 249 $addresslist = $data['addresslist']; 250 251 $subscriptions = $this->subscribers($id, null, 'every'); 252 253 $result = []; 254 foreach ($subscriptions as $users) { 255 foreach ($users as $user => $info) { 256 $userinfo = $auth->getUserData($user); 257 if ($userinfo === false) { 258 continue; 259 } 260 if (!$userinfo['mail']) { 261 continue; 262 } 263 if (!$self && $user == $INPUT->server->str('REMOTE_USER')) { 264 continue; 265 } //skip our own changes 266 267 $level = auth_aclcheck($id, $user, $userinfo['grps']); 268 if ($level >= AUTH_READ) { 269 if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere 270 $result[$user] = $userinfo['mail']; 271 } 272 } 273 } 274 } 275 $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ','); 276 } 277 278 /** 279 * Return the subscription meta file for the given ID 280 * 281 * @author Adrian Lang <lang@cosmocode.de> 282 * 283 * @param string $id The target page or namespace, specified by id; Namespaces 284 * are identified by appending a colon. 285 * 286 * @return string 287 */ 288 protected function file($id) 289 { 290 $meta_fname = '.mlist'; 291 if (str_ends_with($id, ':')) { 292 $meta_froot = getNS($id); 293 $meta_fname = '/' . $meta_fname; 294 } else { 295 $meta_froot = $id; 296 } 297 return metaFN((string)$meta_froot, $meta_fname); 298 } 299} 300