xref: /dokuwiki/inc/Subscriptions/BulkSubscriptionSender.php (revision fe15e2c063a38f65804c55e581c72b96ac36edf7)
1<?php
2
3
4namespace dokuwiki\Subscriptions;
5
6
7use dokuwiki\ChangeLog\PageChangeLog;
8use dokuwiki\Extension\AuthPlugin;
9use dokuwiki\Input\Input;
10use Exception;
11
12class BulkSubscriptionSender extends SubscriptionSender
13{
14
15    /**
16     * Send digest and list subscriptions
17     *
18     * This sends mails to all subscribers that have a subscription for namespaces above
19     * the given page if the needed $conf['subscribe_time'] has passed already.
20     *
21     * This function is called form lib/exe/indexer.php
22     *
23     * @param string $page
24     * @return int number of sent mails
25     * @throws Exception
26     */
27    public function sendBulk($page)
28    {
29        $subscriberManager = new SubscriberManager();
30        if (!$subscriberManager->isenabled()) {
31            return 0;
32        }
33
34        /** @var AuthPlugin $auth */
35        global $auth;
36        global $conf;
37        global $USERINFO;
38        /** @var Input $INPUT */
39        global $INPUT;
40        $count = 0;
41
42        $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']);
43
44        // remember current user info
45        $olduinfo = $USERINFO;
46        $olduser = $INPUT->server->str('REMOTE_USER');
47
48        foreach ($subscriptions as $target => $users) {
49            if (!$this->lock($target)) {
50                continue;
51            }
52
53            foreach ($users as $user => $info) {
54                [$style, $lastupdate] = $info;
55
56                $lastupdate = (int)$lastupdate;
57                if ($lastupdate + $conf['subscribe_time'] > time()) {
58                    // Less than the configured time period passed since last
59                    // update.
60                    continue;
61                }
62
63                // Work as the user to make sure ACLs apply correctly
64                $USERINFO = $auth->getUserData($user);
65                $INPUT->server->set('REMOTE_USER', $user);
66                if ($USERINFO === false) {
67                    continue;
68                }
69                if (!$USERINFO['mail']) {
70                    continue;
71                }
72
73                if (substr($target, -1, 1) === ':') {
74                    // subscription target is a namespace, get all changes within
75                    $changes = getRecentsSince($lastupdate, null, getNS($target));
76                } else {
77                    // single page subscription, check ACL ourselves
78                    if (auth_quickaclcheck($target) < AUTH_READ) {
79                        continue;
80                    }
81                    $meta = p_get_metadata($target);
82                    $changes = [$meta['last_change']];
83                }
84
85                // Filter out pages only changed in small and own edits
86                $change_ids = [];
87                foreach ($changes as $rev) {
88                    $n = 0;
89                    $pagelog = new PageChangeLog($rev['id']);
90                    while (
91                        !is_null($rev) && $rev['date'] >= $lastupdate &&
92                        ($INPUT->server->str('REMOTE_USER') === $rev['user'] ||
93                            $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)
94                    ) {
95                        $revisions = $pagelog->getRevisions($n++, 1);
96                        $rev = ($revisions !== []) ? $pagelog->getRevisionInfo($revisions[0]) : null;
97                    }
98
99                    if (!is_null($rev) && $rev['date'] >= $lastupdate) {
100                        // Some change was not a minor one and not by myself
101                        $change_ids[] = $rev['id'];
102                    }
103                }
104
105                // send it
106                if ($style === 'digest') {
107                    foreach ($change_ids as $change_id) {
108                        $this->sendDigest(
109                            $USERINFO['mail'],
110                            $change_id,
111                            $lastupdate
112                        );
113                        $count++;
114                    }
115                } elseif ($style === 'list') {
116                    $this->sendList($USERINFO['mail'], $change_ids, $target);
117                    $count++;
118                }
119                // TODO: Handle duplicate subscriptions.
120
121                // Update notification time.
122                $subscriberManager->add($target, $user, $style, time());
123            }
124            $this->unlock($target);
125        }
126
127        // restore current user info
128        $USERINFO = $olduinfo;
129        $INPUT->server->set('REMOTE_USER', $olduser);
130        return $count;
131    }
132
133    /**
134     * Lock subscription info
135     *
136     * We don't use io_lock() her because we do not wait for the lock and use a larger stale time
137     *
138     * @param string $id The target page or namespace, specified by id; Namespaces
139     *                   are identified by appending a colon.
140     *
141     * @return bool true, if you got a succesful lock
142     * @author Adrian Lang <lang@cosmocode.de>
143     */
144    protected function lock($id)
145    {
146        global $conf;
147
148        $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';
149
150        if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) {
151            // looks like a stale lock - remove it
152            @rmdir($lock);
153        }
154
155        // try creating the lock directory
156        if (!@mkdir($lock)) {
157            return false;
158        }
159
160        if ($conf['dperm']) {
161            chmod($lock, $conf['dperm']);
162        }
163        return true;
164    }
165
166    /**
167     * Unlock subscription info
168     *
169     * @param string $id The target page or namespace, specified by id; Namespaces
170     *                   are identified by appending a colon.
171     *
172     * @return bool
173     * @author Adrian Lang <lang@cosmocode.de>
174     */
175    protected function unlock($id)
176    {
177        global $conf;
178        $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';
179        return @rmdir($lock);
180    }
181
182    /**
183     * Send a digest mail
184     *
185     * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff()
186     * but determines the last known revision first
187     *
188     * @param string $subscriber_mail The target mail address
189     * @param string $id              The ID
190     * @param int    $lastupdate      Time of the last notification
191     *
192     * @return bool
193     * @author Adrian Lang <lang@cosmocode.de>
194     *
195     */
196    protected function sendDigest($subscriber_mail, $id, $lastupdate)
197    {
198        $pagelog = new PageChangeLog($id);
199        $n = 0;
200        do {
201            $rev = $pagelog->getRevisions($n++, 1);
202            $rev = ($rev !== []) ? $rev[0] : null;
203        } while (!is_null($rev) && $rev > $lastupdate);
204
205        // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better
206        $pageSubSender = new PageSubscriptionSender($this->mailer);
207        return $pageSubSender->sendPageDiff(
208            $subscriber_mail,
209            'subscr_digest',
210            $id,
211            $rev
212        );
213    }
214
215    /**
216     * Send a list mail
217     *
218     * Sends a list mail showing a list of changed pages.
219     *
220     * @param string $subscriber_mail The target mail address
221     * @param array  $ids             Array of ids
222     * @param string $ns_id           The id of the namespace
223     *
224     * @return bool true if a mail was sent
225     * @author Adrian Lang <lang@cosmocode.de>
226     *
227     */
228    protected function sendList($subscriber_mail, $ids, $ns_id)
229    {
230        if ($ids === []) {
231            return false;
232        }
233
234        $tlist = '';
235        $hlist = '<ul>';
236        foreach ($ids as $id) {
237            $link = wl($id, [], true);
238            $tlist .= '* ' . $link . NL;
239            $hlist .= '<li><a href="' . $link . '">' . hsc($id) . '</a></li>' . NL;
240        }
241        $hlist .= '</ul>';
242
243        $id = prettyprint_id($ns_id);
244        $trep = [
245            'DIFF' => rtrim($tlist),
246            'PAGE' => $id,
247            'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'),
248        ];
249        $hrep = [
250            'DIFF' => $hlist,
251        ];
252
253        return $this->send(
254            $subscriber_mail,
255            'subscribe_list',
256            $ns_id,
257            'subscr_list',
258            $trep,
259            $hrep
260        );
261    }
262}
263