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