1 <?php
2 
3 namespace dokuwiki\Subscriptions;
4 
5 use dokuwiki\Extension\AuthPlugin;
6 use dokuwiki\Input\Input;
7 use Exception;
8 
9 class 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