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