1<?php 2 3namespace dokuwiki\Subscriptions; 4 5use dokuwiki\Input\Input; 6use Exception; 7 8class SubscriberManager 9{ 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 */ 80 public function remove($id, $user = null, $style = null, $data = null) 81 { 82 if (!$this->isenabled()) { 83 return false; 84 } 85 86 $file = $this->file($id); 87 if (!file_exists($file)) { 88 return true; 89 } 90 91 $regexBuilder = new SubscriberRegexBuilder(); 92 $re = $regexBuilder->buildRegex($user, $style, $data); 93 return io_deleteFromFile($file, $re, true); 94 } 95 96 /** 97 * Get data for $INFO['subscribed'] 98 * 99 * $INFO['subscribed'] is either false if no subscription for the current page 100 * and user is in effect. Else it contains an array of arrays with the fields 101 * “target”, “style”, and optionally “data”. 102 * 103 * @author Adrian Lang <lang@cosmocode.de> 104 * 105 * @param string $id Page ID, defaults to global $ID 106 * @param string $user User, defaults to $_SERVER['REMOTE_USER'] 107 * 108 * @return array|false 109 */ 110 public function userSubscription($id = '', $user = '') 111 { 112 if (!$this->isenabled()) { 113 return false; 114 } 115 116 global $ID; 117 /** @var Input $INPUT */ 118 global $INPUT; 119 if (!$id) { 120 $id = $ID; 121 } 122 if (!$user) { 123 $user = $INPUT->server->str('REMOTE_USER'); 124 } 125 126 if (empty($user)) { 127 // not logged in 128 return false; 129 } 130 131 $subs = $this->subscribers($id, $user); 132 if ($subs === []) { 133 return false; 134 } 135 136 $result = []; 137 foreach ($subs as $target => $info) { 138 $result[] = [ 139 'target' => $target, 140 'style' => $info[$user][0], 141 'data' => $info[$user][1], 142 ]; 143 } 144 145 return $result; 146 } 147 148 /** 149 * Recursively search for matching subscriptions 150 * 151 * This function searches all relevant subscription files for a page or 152 * namespace. 153 * 154 * @author Adrian Lang <lang@cosmocode.de> 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 */ 163 public function subscribers($page, $user = null, $style = null, $data = null) 164 { 165 if (!$this->isenabled()) { 166 return []; 167 } 168 169 // Construct list of files which may contain relevant subscriptions. 170 $files = [':' => $this->file(':')]; 171 do { 172 $files[$page] = $this->file($page); 173 $page = getNS(rtrim($page, ':')) . ':'; 174 } while ($page !== ':'); 175 176 $regexBuilder = new SubscriberRegexBuilder(); 177 $re = $regexBuilder->buildRegex($user, $style, $data); 178 179 // Handle files. 180 $result = []; 181 foreach ($files as $target => $file) { 182 if (!file_exists($file)) { 183 continue; 184 } 185 186 $lines = file($file); 187 foreach ($lines as $line) { 188 // fix old style subscription files 189 if (strpos($line, ' ') === false) { 190 $line = trim($line) . " every\n"; 191 } 192 193 // check for matching entries 194 if (!preg_match($re, $line, $m)) { 195 continue; 196 } 197 198 // if no last sent is set, use 0 199 if (!isset($m[3])) { 200 $m[3] = 0; 201 } 202 203 $u = rawurldecode($m[1]); // decode the user name 204 if (!isset($result[$target])) { 205 $result[$target] = []; 206 } 207 $result[$target][$u] = [$m[2], $m[3]]; // add to result 208 } 209 } 210 return array_reverse($result); 211 } 212 213 /** 214 * Default callback for COMMON_NOTIFY_ADDRESSLIST 215 * 216 * Aggregates all email addresses of user who have subscribed the given page with 'every' style 217 * 218 * @author Adrian Lang <lang@cosmocode.de> 219 * @author Steven Danz <steven-danz@kc.rr.com> 220 * 221 * @todo move the whole functionality into this class, trigger SUBSCRIPTION_NOTIFY_ADDRESSLIST instead, 222 * use an array for the addresses within it 223 * 224 * @param array &$data Containing the entries: 225 * - $id (the page id), 226 * - $self (whether the author should be notified, 227 * - $addresslist (current email address list) 228 * - $replacements (array of additional string substitutions, @KEY@ to be replaced by value) 229 */ 230 public function notifyAddresses(&$data) 231 { 232 if (!$this->isenabled()) { 233 return; 234 } 235 236 /** @var DokuWiki_Auth_Plugin $auth */ 237 global $auth; 238 global $conf; 239 /** @var \Input $INPUT */ 240 global $INPUT; 241 242 $id = $data['id']; 243 $self = $data['self']; 244 $addresslist = $data['addresslist']; 245 246 $subscriptions = $this->subscribers($id, null, 'every'); 247 248 $result = []; 249 foreach ($subscriptions as $users) { 250 foreach ($users as $user => $info) { 251 $userinfo = $auth->getUserData($user); 252 if ($userinfo === false) { 253 continue; 254 } 255 if (!$userinfo['mail']) { 256 continue; 257 } 258 if (!$self && $user == $INPUT->server->str('REMOTE_USER')) { 259 continue; 260 } //skip our own changes 261 262 $level = auth_aclcheck($id, $user, $userinfo['grps']); 263 if ($level >= AUTH_READ) { 264 if (strcasecmp($userinfo['mail'], $conf['notify']) != 0) { //skip user who get notified elsewhere 265 $result[$user] = $userinfo['mail']; 266 } 267 } 268 } 269 } 270 $data['addresslist'] = trim($addresslist . ',' . implode(',', $result), ','); 271 } 272 273 /** 274 * Return the subscription meta file for the given ID 275 * 276 * @author Adrian Lang <lang@cosmocode.de> 277 * 278 * @param string $id The target page or namespace, specified by id; Namespaces 279 * are identified by appending a colon. 280 * 281 * @return string 282 */ 283 protected function file($id) 284 { 285 $meta_fname = '.mlist'; 286 if ((substr($id, -1, 1) === ':')) { 287 $meta_froot = getNS($id); 288 $meta_fname = '/' . $meta_fname; 289 } else { 290 $meta_froot = $id; 291 } 292 return metaFN((string)$meta_froot, $meta_fname); 293 } 294} 295