xref: /dokuwiki/inc/Subscriptions/SubscriberManager.php (revision 8553d24d33ab5f260c6e19959de764dd8472d438)
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