Happy New Year !
[platal.git] / include / newsletter.inc.php
CommitLineData
0337d704 1<?php
2/***************************************************************************
ba6ae046 3 * Copyright (C) 2003-2013 Polytechnique.org *
0337d704 4 * http://opensource.polytechnique.org/ *
5 * *
6 * This program is free software; you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation; either version 2 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program; if not, write to the Free Software *
18 * Foundation, Inc., *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
21
84163d58
RB
22// {{{ class MailNotFound
23
24class MailNotFound extends Exception {
25}
26
27// }}}
0337d704 28
0337d704 29// {{{ class NewsLetter
30
84163d58 31class NewsLetter
0337d704 32{
84163d58
RB
33 public $id; // ID of the NL (in table newsletters)
34 public $group; // Short name of the group corresponding to the NL
35 public $group_id; // ID of that group
36 public $name; // Name of the NL (e.g "Lettre de Polytechnique.org", ...)
37 public $cats; // List of all categories for this NL
38 public $criteria; // PlFlagSet of allowed filters for recipient selection
39
40 protected $custom_css = false;
0337d704 41
84163d58
RB
42 // Base name to use instead of the group short name for NLs without a custom CSS
43 const FORMAT_DEFAULT_GROUP = 'default';
44
45 // Diminutif of X.net groups with a specific NL view
46 const GROUP_XORG = 'Polytechnique.org';
47 const GROUP_AX = 'AX';
48 const GROUP_EP = 'Ecole';
87d116c4 49 const GROUP_FX = 'FX';
84163d58 50
5e9a29ed
SJ
51 // Searches on mutiple fields
52 const SEARCH_ALL = 'all';
53 const SEARCH_TITLE = 'title';
54
55
84163d58
RB
56 // {{{ Constructor, NewsLetter retrieval (forGroup, getAll)
57
58 public function __construct($id)
0337d704 59 {
84163d58
RB
60 // Load NL data
61 $res = XDB::query('SELECT nls.group_id, g.diminutif AS group_name,
62 nls.name AS nl_name, nls.custom_css, nls.criteria
63 FROM newsletters AS nls
64 LEFT JOIN groups AS g ON (nls.group_id = g.id)
65 WHERE nls.id = {?}',
66 $id);
67 if (!$res->numRows()) {
68 throw new MailNotFound();
69 }
70
71 $data = $res->fetchOneAssoc();
72 $this->id = $id;
73 $this->group_id = $data['group_id'];
74 $this->group = $data['group_name'];
75 $this->name = $data['nl_name'];
76 $this->custom_css = $data['custom_css'];
77 $this->criteria = new PlFlagSet($data['criteria']);
78
79 // Load the categories
80 $res = XDB::iterRow(
81 'SELECT cid, title
82 FROM newsletter_cat
83 WHERE nlid = {?}
84 ORDER BY pos', $id);
85 while (list($cid, $title) = $res->next()) {
86 $this->cats[$cid] = $title;
87 }
88 }
89
90 /** Retrieve the NL associated with a given group.
91 * @p $group Short name of the group
92 * @return A NewsLetter object, or null if the group doesn't have a NL.
93 */
94 public static function forGroup($group)
95 {
96 $res = XDB::query('SELECT nls.id
97 FROM newsletters AS nls
98 LEFT JOIN groups AS g ON (nls.group_id = g.id)
99 WHERE g.diminutif = {?}', $group);
100 if (!$res->numRows()) {
101 return null;
102 }
103 return new NewsLetter($res->fetchOneCell());
104 }
105
106 /** Retrieve all newsletters
107 * @return An array of $id => NewsLetter objects
108 */
63273514 109 public static function getAll($sort = 'id', $order = 'ASC')
84163d58 110 {
63273514
SJ
111 $res = XDB::fetchAllAssoc('SELECT n.id, g.nom AS group_name, n.name, n.custom_css, n.criteria, g.diminutif AS group_link
112 FROM newsletters AS n
113 INNER JOIN groups AS g ON (n.group_id = g.id)
114 ORDER BY ' . $sort . ' ' . $order);
115 return $res;
84163d58
RB
116 }
117
118 // }}}
119 // {{{ Issue retrieval
120
121 /** Retrieve all issues which should be sent
122 * @return An array of NLIssue objects to send (i.e state = 'new' and send_before <= today)
123 */
124 public static function getIssuesToSend()
125 {
126 $res = XDB::query('SELECT id
127 FROM newsletter_issues
128 WHERE state = \'pending\' AND send_before <= NOW()');
129 $issues = array();
130 foreach ($res->fetchColumn() as $id) {
131 $issues[$id] = new NLIssue($id);
132 }
133 return $issues;
134 }
135
136 /** Retrieve a given issue of this NewsLetter
137 * @p $name Name or ID of the issue to retrieve.
138 * @return A NLIssue object.
139 *
140 * $name may be either a short_name, an ID or the special value 'last' which
141 * selects the latest sent NL.
142 * If $name is null, this will retrieve the current pending NL.
143 */
144 public function getIssue($name = null, $only_sent = true)
145 {
146 if ($name) {
147 if ($name == 'last') {
148 if ($only_sent) {
149 $where = 'state = \'sent\' AND ';
150 } else {
151 $where = '';
152 }
153 $res = XDB::query('SELECT MAX(id)
154 FROM newsletter_issues
155 WHERE ' . $where . ' nlid = {?}',
156 $this->id);
157 } else {
158 $res = XDB::query('SELECT id
159 FROM newsletter_issues
160 WHERE nlid = {?} AND (id = {?} OR short_name = {?})',
161 $this->id, $name, $name);
ed76a506 162 }
ed76a506 163 if (!$res->numRows()) {
84163d58
RB
164 throw new MailNotFound();
165 }
166 $id = $res->fetchOneCell();
167 } else {
168 $query = XDB::format('SELECT id
169 FROM newsletter_issues
170 WHERE nlid = {?} AND state = \'new\'
171 ORDER BY id DESC', $this->id);
172 $res = XDB::query($query);
173 if ($res->numRows()) {
174 $id = $res->fetchOneCell();
175 } else {
176 // Create a new, empty issue, and return it
177 $id = $this->createPending();
ed76a506 178 }
ad6ca77d 179 }
84163d58 180
26ba053e 181 return new NLIssue($id, $this);
84163d58
RB
182 }
183
184 /** Create a new, empty, pending newsletter issue
185 * @p $nlid The id of the NL for which a new pending issue should be created.
186 * @return Id of the newly created issue.
187 */
188 public function createPending()
189 {
190 XDB::execute('INSERT INTO newsletter_issues
191 SET nlid = {?}, state=\'new\', date=NOW(),
192 title=\'to be continued\',
193 mail_title=\'to be continued\'',
194 $this->id);
195 return XDB::insertId();
196 }
197
198 /** Return all sent issues of this newsletter.
199 * @return An array of (id => NLIssue)
200 */
201 public function listSentIssues($check_user = false, $user = null)
202 {
203 if ($check_user && $user == null) {
204 $user = S::user();
205 }
206
207 $res = XDB::query('SELECT id
208 FROM newsletter_issues
209 WHERE nlid = {?} AND state = \'sent\'
210 ORDER BY date DESC', $this->id);
211 $issues = array();
212 foreach ($res->fetchColumn() as $id) {
213 $issue = new NLIssue($id, $this, false);
214 if (!$check_user || $issue->checkUser($user)) {
215 $issues[$id] = $issue;
216 }
217 }
218 return $issues;
219 }
220
221 /** Return all issues of this newsletter, including invalid and sent.
222 * @return An array of (id => NLIssue)
223 */
224 public function listAllIssues()
225 {
226 $res = XDB::query('SELECT id
227 FROM newsletter_issues
228 WHERE nlid = {?}
229 ORDER BY FIELD(state, \'pending\', \'new\') DESC, date DESC', $this->id);
230 $issues = array();
231 foreach ($res->fetchColumn() as $id) {
232 $issues[$id] = new NLIssue($id, $this, false);
233 }
234 return $issues;
235 }
236
237 /** Return the latest pending issue of the newsletter.
b8bcbe02 238 * @p $create Whether to create an empty issue if no pending issue exist.
84163d58
RB
239 * @return Either null, or a NL object.
240 */
b8bcbe02 241 public function getPendingIssue($create = false)
84163d58
RB
242 {
243 $res = XDB::query('SELECT MAX(id)
244 FROM newsletter_issues
245 WHERE nlid = {?} AND state = \'new\'',
246 $this->id);
8dc39e6a
RB
247 $id = $res->fetchOneCell();
248 if ($id != null) {
84163d58 249 return new NLIssue($id, $this);
b8bcbe02
RB
250 } else if ($create) {
251 $id = $this->createPending();
252 return new NLIssue($id, $this);
84163d58
RB
253 } else {
254 return null;
255 }
256 }
257
5e9a29ed
SJ
258 /** Returns a list of either issues or articles corresponding to the search.
259 * @p $search The searched pattern.
260 * @p $field The fields where to search, if none given, search in all possible fields.
261 * @return The list of object found.
262 */
263 public function issueSearch($search, $field, $user)
264 {
265 $search = XDB::formatWildcards(XDB::WILDCARD_CONTAINS, $search);
266 if ($field == self::SEARCH_ALL) {
267 $where = '(title ' . $search . ' OR mail_title ' . $search . ' OR head ' . $search . ' OR signature ' . $search . ')';
268 } elseif ($field == self::SEARCH_TITLE) {
269 $where = '(title ' . $search . ' OR mail_title ' . $search . ')';
270 } else {
271 $where = $field . $search;
272 }
273 $list = XDB::fetchColumn('SELECT DISTINCT(id)
274 FROM newsletter_issues
275 WHERE nlid = {?} AND state = \'sent\' AND ' . $where . '
276 ORDER BY date DESC',
277 $this->id);
278
279 $issues = array();
280 foreach ($list as $id) {
281 $issue = new NLIssue($id, $this, false);
282 if ($issue->checkUser($user)) {
283 $issues[] = $issue;
284 }
285 }
286 return $issues;
287 }
288
289 public function articleSearch($search, $field, $user)
290 {
291 $search = XDB::formatWildcards(XDB::WILDCARD_CONTAINS, $search);
292 if ($field == self::SEARCH_ALL) {
293 $where = '(a.title ' . $search . ' OR a.body ' . $search . ' OR a.append ' . $search . ')';
294 } else {
295 $where = 'a.' . $field . $search;
296 }
297 $list = XDB::fetchAllAssoc('SELECT i.short_name, a.aid, i.id, a.title
298 FROM newsletter_art AS a
299 INNER JOIN newsletter_issues AS i ON (a.id = i.id)
300 WHERE i.nlid = {?} AND i.state = \'sent\' AND ' . $where . '
301 GROUP BY a.id, a.aid
302 ORDER BY i.date DESC, a.aid',
303 $this->id);
304
305 $articles = array();
306 foreach ($list as $item) {
307 $issue = new NLIssue($item['id'], $this, false);
308 if ($issue->checkUser($user)) {
309 $articles[] = $item;
310 }
311 }
312 return $articles;
313 }
314
84163d58
RB
315 // }}}
316 // {{{ Subscription related function
317
318 /** Unsubscribe a user from this newsletter
319 * @p $uid UID to unsubscribe from the newsletter; if null, use current user.
320 * @p $hash True if the uid is actually a hash.
321 * @return True if the user was successfully unsubscribed.
322 */
602119a2 323 public function unsubscribe($issue_id = null, $uid = null, $hash = false)
84163d58
RB
324 {
325 if (is_null($uid) && $hash) {
326 // Unable to unsubscribe from an empty hash
327 return false;
328 }
329 $user = is_null($uid) ? S::user()->id() : $uid;
330 $field = $hash ? 'hash' : 'uid';
331 $res = XDB::query('SELECT uid
332 FROM newsletter_ins
333 WHERE nlid = {?} AND ' . $field . ' = {?}',
334 $this->id, $user);
335 if (!$res->numRows()) {
336 // No subscribed user with that UID/hash
337 return false;
338 }
339 $user = $res->fetchOneCell();
340
341 XDB::execute('DELETE FROM newsletter_ins
342 WHERE nlid = {?} AND uid = {?}',
343 $this->id, $user);
602119a2
SJ
344 if (!is_null($issue_id)) {
345 XDB::execute('UPDATE newsletter_issues
346 SET unsubscribe = unsubscribe + 1
347 WHERE id = {?}',
348 $id);
349 }
84163d58
RB
350 return true;
351 }
352
353 /** Subscribe a user to a newsletter
354 * @p $user User to subscribe to the newsletter; if null, use current user.
355 */
356 public function subscribe($user = null)
357 {
358 if (is_null($user)) {
359 $user = S::user();
360 }
361 if (self::maySubscribe($user)) {
362 XDB::execute('INSERT IGNORE INTO newsletter_ins (nlid, uid, last, hash)
e4b8520c 363 VALUES ({?}, {?}, NULL, hash)',
84163d58
RB
364 $this->id, $user->id());
365 }
366 }
367
e4b8520c
RB
368 /** Subscribe a batch of users to a newsletter.
369 * This skips 'maySubscribe' test.
370 *
371 * @p $user_ids Array of user IDs to subscribe to the newsletter.
372 */
373 public function bulkSubscribe($user_ids)
374 {
375 // TODO: use a 'bulkMaySubscribe'.
376 XDB::execute('INSERT IGNORE INTO newsletter_ins (nlid, uid, last, hash)
377 SELECT {?}, a.uid, NULL, NULL
378 FROM accounts AS a
379 WHERE a.uid IN {?}',
380 $this->id, $user_ids);
381 }
382
84163d58
RB
383 /** Retrieve subscription state of a user
384 * @p $user Target user; if null, use current user.
385 * @return Boolean: true if the user has subscribed to the NL.
386 */
387 public function subscriptionState($user = null)
388 {
389 if (is_null($user)) {
390 $user = S::user();
391 }
392 $res = XDB::query('SELECT 1
393 FROM newsletter_ins
394 WHERE nlid = {?} AND uid = {?}',
395 $this->id, $user->id());
396 return ($res->numRows() == 1);
397 }
398
399 /** Get the count of subscribers to the NL.
400 * @return Number of subscribers.
401 */
ccabe1ac 402 public function subscriberCount($lost = null, $sex = null, $grade = null, $first_promo = null, $last_promo = null)
84163d58 403 {
ccabe1ac
SJ
404 $cond = new PFC_And(new UFC_NLSubscribed($this->id));
405 if (!is_null($sex)) {
406 $cond->addChild(new UFC_Sex($sex));
407 }
408 if (!is_null($grade)) {
409 $cond->addChild(new UFC_Promo('>=', $grade, $first_promo));
410 $cond->addChild(new UFC_Promo('<=', $grade, $last_promo));
411 }
412 if (!($lost === null)) {
413 if ($lost === true) {
414 $cond->addChild(new PFC_Not(new UFC_HasEmailRedirect()));
415 } else {
416 $cond->addChild(new UFC_HasEmailRedirect());
417 }
418 }
419 $uf = new UserFilter($cond);
332cf4b9 420 return $uf->getTotalCount();
84163d58
RB
421 }
422
b0653ad8
SJ
423 /** Get the count of subscribers with non valid redirection.
424 */
ccabe1ac 425 public function lostSubscriberCount($sex = null)
b0653ad8 426 {
ccabe1ac 427 return $this->subscriberCount(true, $sex);
b0653ad8
SJ
428 }
429
84163d58
RB
430 /** Get the number of subscribers to the NL whose last received mailing was $last.
431 * @p $last ID of the issue for which subscribers should be counted.
432 * @return Number of subscribers
433 */
434 public function subscriberCountForLast($last)
435 {
436 return XDB::fetchOneCell('SELECT COUNT(uid)
437 FROM newsletter_ins
438 WHERE nlid = {?} AND last = {?}', $this->id, $last);
439 }
440
441 /** Retrieve the list of newsletters a user has subscribed to
442 * @p $user User whose subscriptions should be retrieved (if null, use session user).
443 * @return Array of newsletter IDs
444 */
445 public static function getUserSubscriptions($user = null)
446 {
447 if (is_null($user)) {
448 $user = S::user();
449 }
450 $res = XDB::query('SELECT nlid
451 FROM newsletter_ins
452 WHERE uid = {?}',
453 $user->id());
454 return $res->fetchColumn();
455 }
456
457 /** Retrieve the UserFilterBuilder for subscribers to this NL.
458 * This is the place where NL-specific filters may be allowed or prevented.
459 * @p $envprefix Prefix to use for env fields (cf. UserFilterBuilder)
460 * @return A UserFilterBuilder object using the given env prefix
461 */
462 public function getSubscribersUFB($envprefix = '')
463 {
464 require_once 'ufbuilder.inc.php';
465 return new UFB_NewsLetter($this->criteria, $envprefix);
466 }
467
468 // }}}
469 // {{{ Permissions related functions
470
471 /** For later use: check whether a given user may subscribe to this newsletter.
472 * @p $user User whose access should be checked
473 * @return Boolean: whether the user may subscribe to the NL.
474 */
475 public function maySubscribe($user = null)
476 {
477 return true;
478 }
479
480 /** Whether a given user may edit this newsletter
481 * @p $uid UID of the user whose perms should be checked (if null, use current user)
482 * @return Boolean: whether the user may edit the NL
483 */
484 public function mayEdit($user = null)
485 {
486 if (is_null($user)) {
487 $user = S::user();
488 }
489 if ($user->checkPerms('admin')) {
490 return true;
491 }
492 $res = XDB::query('SELECT perms
493 FROM group_members
494 WHERE asso_id = {?} AND uid = {?}',
495 $this->group_id, $user->id());
496 return ($res->numRows() && $res->fetchOneCell() == 'admin');
497 }
498
499 /** Whether a given user may submit articles to this newsletter using X.org validation system
500 * @p $user User whose access should be checked (if null, use current user)
501 * @return Boolean: whether the user may submit articles
502 */
503 public function maySubmit($user = null)
504 {
505 // Submission of new articles is only enabled for the X.org NL (and forbidden when viewing issues on X.net)
506 return ($this->group == self::GROUP_XORG && !isset($GLOBALS['IS_XNET_SITE']));
507 }
508
509 // }}}
510 // {{{ Display-related functions: cssFile, tplFile, prefix, admin_prefix, admin_links_enabled, automatic_mailings_enabled
511
512 /** Get the name of the css file used to display this newsletter.
513 */
514 public function cssFile()
515 {
516 if ($this->custom_css) {
517 $base = $this->group;
518 } else {
519 $base = self::FORMAT_DEFAULT_GROUP;
520 }
521 return 'nl.' . $base . '.css';
522 }
523
524 /** Get the name of the template file used to display this newsletter.
525 */
526 public function tplFile()
527 {
528 if ($this->custom_css) {
529 $base = $this->group;
530 } else {
531 $base = self::FORMAT_DEFAULT_GROUP;
532 }
533 return 'newsletter/nl.' . $base . '.mail.tpl';
534 }
535
536 /** Get the prefix leading to the page for this NL
537 * Only X.org / AX / X groups may be seen on X.org.
538 */
b6d52898 539 public function prefix($enforce_xnet=true, $with_group=true)
84163d58
RB
540 {
541 if (!empty($GLOBALS['IS_XNET_SITE'])) {
b6d52898
RB
542 if ($with_group) {
543 return $this->group . '/nl';
544 } else {
545 return 'nl';
546 }
84163d58
RB
547 }
548 switch ($this->group) {
549 case self::GROUP_XORG:
550 return 'nl';
551 case self::GROUP_AX:
552 return 'ax';
553 case self::GROUP_EP:
554 return 'epletter';
87d116c4
RB
555 case self::GROUP_FX:
556 return 'fxletter';
84163d58
RB
557 default:
558 // Don't display groups NLs on X.org
c8fc2813 559 assert(!$enforce_xnet);
84163d58
RB
560 }
561 }
562
563 /** Get the prefix to use for all 'admin' pages of this NL.
564 */
b6d52898 565 public function adminPrefix($enforce_xnet=true, $with_group=true)
84163d58
RB
566 {
567 if (!empty($GLOBALS['IS_XNET_SITE'])) {
b6d52898
RB
568 if ($with_group) {
569 return $this->group . '/admin/nl';
570 } else {
571 return 'admin/nl';
572 }
84163d58
RB
573 }
574 switch ($this->group) {
575 case self::GROUP_XORG:
576 return 'admin/newsletter';
577 case self::GROUP_AX:
578 return 'ax/admin';
579 case self::GROUP_EP:
580 return 'epletter/admin';
87d116c4
RB
581 case self::GROUP_FX:
582 return 'fxletter/admin';
84163d58
RB
583 default:
584 // Don't display groups NLs on X.org
e9700d3e 585 assert(!$enforce_xnet);
84163d58
RB
586 }
587 }
588
a8d1ec11
SJ
589 /** Get the prefix to use for all 'stat' pages of this NL.
590 */
591 public function statPrefix($enforce_xnet = true, $with_group = true)
592 {
593 if (!empty($GLOBALS['IS_XNET_SITE'])) {
594 if ($with_group) {
595 return $this->group . '/stat/nl';
596 } else {
597 return 'stat/nl';
598 }
599 }
600 switch ($this->group) {
601 case self::GROUP_XORG:
602 return 'stat/newsletter';
603 case self::GROUP_AX:
604 return 'ax/stat';
605 case self::GROUP_EP:
606 return 'epletter/stat';
87d116c4
RB
607 case self::GROUP_FX:
608 return 'fxletter/stat';
a8d1ec11
SJ
609 default:
610 // Don't display groups NLs on X.org
611 assert(!$enforce_xnet);
612 }
613 }
614
5a8a7dad
SJ
615 /** Get links for nl pages.
616 */
617 public function adminLinks()
618 {
619 return array(
620 'index' => array('link' => $this->prefix(), 'title' => 'Archives'),
a8d1ec11
SJ
621 'admin' => array('link' => $this->adminPrefix(), 'title' => 'Administrer'),
622 'stats' => array('link' => $this->statPrefix(), 'title' => 'Statistiques')
5a8a7dad
SJ
623 );
624 }
625
84163d58
RB
626 /** Hack used to remove "admin" links on X.org page on X.net
627 * The 'admin' links are enabled for all pages, except for X.org when accessing NL through X.net
628 */
629 public function adminLinksEnabled()
630 {
631 return ($this->group != self::GROUP_XORG || !isset($GLOBALS['IS_XNET_SITE']));
632 }
633
634 /** Automatic mailings are disabled for X.org NL.
635 */
636 public function automaticMailingEnabled()
637 {
638 return $this->group != self::GROUP_XORG;
639 }
640
641 public function hasCustomCss()
642 {
643 return $this->custom_css;
644 }
645
be166df9
SJ
646 public function canSyncWithGroup()
647 {
648 switch ($this->group) {
649 case self::GROUP_XORG:
650 case self::GROUP_AX:
651 case self::GROUP_EP:
87d116c4 652 case self::GROUP_FX:
be166df9
SJ
653 return false;
654 default:
655 return true;
656 }
657 }
658
84163d58
RB
659 // }}}
660}
661
662// }}}
663
664// {{{ class NLIssue
665
666// A NLIssue is an issue of a given NewsLetter
667class NLIssue
668{
669 protected $nlid; // Id of the newsletter
670
671 const STATE_NEW = 'new'; // New, currently being edited
672 const STATE_PENDING = 'pending'; // Ready for mailing
673 const STATE_SENT = 'sent'; // Sent
674
675 public $nl; // Related NL
676
677 public $id; // Id of this issue of the newsletter
678 public $shortname; // Shortname for this issue
679 public $title; // Title of this issue
680 public $title_mail; // Title of the email
681 public $state; // State of the issue (one of the STATE_ values)
682 public $sufb; // Environment to use to generate the UFC through an UserFilterBuilder
683
684 public $date; // Date at which this issue was sent
685 public $send_before; // Date at which issue should be sent
686 public $head; // Foreword of the issue (or body for letters with no articles)
687 public $signature; // Signature of the letter
5f0c028a 688 public $reply_to; // Adress to reply to the message (can be empty)
84163d58
RB
689 public $arts = array(); // Articles of the issue
690
691 const BATCH_SIZE = 60; // Number of emails to send every minute.
692
693 // {{{ Constructor, id-related functions
694
695 /** Build a NewsLetter.
696 * @p $id: ID of the issue (unique among all newsletters)
697 * @p $nl: Optional argument containing an already built NewsLetter object.
698 */
699 function __construct($id, $nl = null, $fetch_articles = true)
700 {
701 return $this->fetch($id, $nl, $fetch_articles);
702 }
703
704 protected function refresh()
705 {
706 return $this->fetch($this->id, $this->nl, false);
707 }
708
709 protected function fetch($id, $nl = null, $fetch_articles = true)
710 {
711 // Load this issue
712 $res = XDB::query('SELECT nlid, short_name, date, send_before, state, sufb_json,
5f0c028a 713 title, mail_title, head, signature, reply_to
84163d58
RB
714 FROM newsletter_issues
715 WHERE id = {?}',
716 $id);
717 if (!$res->numRows()) {
9da70671
SJ
718 throw new MailNotFound();
719 }
84163d58
RB
720 $issue = $res->fetchOneAssoc();
721 if ($nl && $nl->id == $issue['nlid']) {
722 $this->nl = $nl;
723 } else {
724 $this->nl = new NewsLetter($issue['nlid']);
725 }
726 $this->id = $id;
b9518dc7 727 $this->nlid = $issue['nlid'];
84163d58
RB
728 $this->shortname = $issue['short_name'];
729 $this->date = $issue['date'];
730 $this->send_before = $issue['send_before'];
731 $this->state = $issue['state'];
732 $this->title = $issue['title'];
733 $this->title_mail = $issue['mail_title'];
734 $this->head = $issue['head'];
735 $this->signature = $issue['signature'];
5f0c028a 736 $this->reply_to = $issue['reply_to'];
84163d58 737 $this->sufb = $this->importJSonStoredUFB($issue['sufb_json']);
0337d704 738
84163d58
RB
739 if ($fetch_articles) {
740 $this->fetchArticles();
741 }
742 }
0337d704 743
84163d58
RB
744 protected function fetchArticles($force = false)
745 {
746 if (count($this->arts) && !$force) {
747 return;
ed76a506 748 }
eaf30d86 749
84163d58 750 // Load the articles
ed76a506 751 $res = XDB::iterRow(
84163d58
RB
752 'SELECT a.title, a.body, a.append, a.aid, a.cid, a.pos
753 FROM newsletter_art AS a
754 INNER JOIN newsletter_issues AS ni USING(id)
755 LEFT JOIN newsletter_cat AS c ON (a.cid = c.cid)
756 WHERE a.id = {?}
757 ORDER BY c.pos, a.pos',
758 $this->id);
ed76a506 759 while (list($title, $body, $append, $aid, $cid, $pos) = $res->next()) {
84163d58
RB
760 $this->arts[$cid][$aid] = new NLArticle($title, $body, $append, $aid, $cid, $pos);
761 }
762 }
763
764 protected function importJSonStoredUFB($json = null)
765 {
766 require_once 'ufbuilder.inc.php';
767 $ufb = $this->nl->getSubscribersUFB();
768 if (is_null($json)) {
769 return new StoredUserFilterBuilder($ufb, new PFC_True());
770 }
771 $export = json_decode($json, true);
772 if (is_null($export)) {
773 PlErrorReport::report("Invalid json while reading NL {$this->nlid}, issue {$this->id}: failed to import '''{$json}'''.");
774 return new StoredUserFilterBuilder($ufb, new PFC_True());
775 }
776 $sufb = new StoredUserFilterBuilder($ufb);
777 $sufb->fillFromExport($export);
778 return $sufb;
779 }
780
781 protected function exportStoredUFBAsJSon()
782 {
783 return json_encode($this->sufb->export());
784 }
785
786 public function id()
787 {
788 return is_null($this->shortname) ? $this->id : $this->shortname;
789 }
790
791 protected function selectId($where)
792 {
793 $res = XDB::query("SELECT IFNULL(ni.short_name, ni.id)
794 FROM newsletter_issues AS ni
795 WHERE ni.state != 'new' AND ni.nlid = {?} AND ${where}
796 LIMIT 1", $this->nl->id);
797 if ($res->numRows() != 1) {
798 return null;
799 }
800 return $res->fetchOneCell();
801 }
802
803 /** Delete this issue
804 * @return True if the issue could be deleted, false otherwise.
805 * Related articles will be deleted through cascading FKs.
806 * If this issue was the last issue for at least one subscriber, the deletion will be aborted.
807 */
808 public function delete()
809 {
810 if ($this->state == self::STATE_NEW) {
811 $res = XDB::query('SELECT COUNT(*)
812 FROM newsletter_ins
813 WHERE last = {?}', $this->id);
814 if ($res->fetchOneCell() > 0) {
815 return false;
816 }
817
818 return XDB::execute('DELETE FROM newsletter_issues
819 WHERE id = {?}', $this->id);
820 } else {
821 return false;
822 }
823 }
824
825 /** Schedule a mailing of this NL
826 * If the 'send_before' field was NULL, it is set to the current time.
827 * @return Boolean Whether the date could be set (false if trying to schedule an already sent NL)
828 */
829 public function scheduleMailing()
830 {
831 if ($this->state == self::STATE_NEW) {
832 $success = XDB::execute('UPDATE newsletter_issues
833 SET state = \'pending\', send_before = IFNULL(send_before, NOW())
834 WHERE id = {?}',
835 $this->id);
836 if ($success) {
d5524e7b
RB
837 global $globals;
838 $mailer = new PlMailer('newsletter/notify_scheduled.mail.tpl');
e9700d3e 839 $mailer->assign('issue', $this);
d5524e7b 840 $mailer->assign('base', $globals->baseurl);
d5524e7b 841 $mailer->send();
84163d58
RB
842 $this->refresh();
843 }
844 return $success;
845 } else {
846 return false;
847 }
848 }
849
850 /** Cancel the scheduled mailing of this NL
851 * @return Boolean: whether the mailing could be cancelled.
852 */
853 public function cancelMailing()
854 {
855 if ($this->state == self::STATE_PENDING) {
856 $success = XDB::execute('UPDATE newsletter_issues
a386dd97 857 SET state = \'new\'
84163d58
RB
858 WHERE id = {?}', $this->id);
859 if ($success) {
860 $this->refresh();
861 }
862 return $success;
863 } else {
864 return false;
865 }
866 }
867
868 /** Helper function for smarty templates: is this issue editable ?
869 */
870 public function isEditable()
871 {
872 return $this->state == self::STATE_NEW;
873 }
874
875 /** Helper function for smarty templates: is the mailing of this issue scheduled ?
876 */
877 public function isPending()
878 {
879 return $this->state == self::STATE_PENDING;
880 }
881
882 /** Helper function for smarty templates: has this issue been sent ?
883 */
884 public function isSent()
885 {
886 return $this->state == self::STATE_SENT;
887 }
888
889 // }}}
890 // {{{ Navigation
891
892 private $id_prev = null;
893 private $id_next = null;
894 private $id_last = null;
895
896 /** Retrieve ID of the previous issue
897 * That value, once fetched, is cached in the private $id_prev variable.
898 * @return ID of the previous issue.
899 */
900 public function prev()
901 {
902 if (is_null($this->id_prev)) {
903 $this->id_prev = $this->selectId(XDB::format("ni.id < {?} ORDER BY ni.id DESC", $this->id));
904 }
905 return $this->id_prev;
906 }
907
908 /** Retrieve ID of the following issue
909 * That value, once fetched, is cached in the private $id_next variable.
910 * @return ID of the following issue.
911 */
912 public function next()
913 {
914 if (is_null($this->id_next)) {
915 $this->id_next = $this->selectId(XDB::format("ni.id > {?} ORDER BY ni.id", $this->id));
916 }
917 return $this->id_next;
918 }
919
920 /** Retrieve ID of the last issue
921 * That value, once fetched, is cached in the private $id_last variable.
922 * @return ID of the last issue.
923 */
924 public function last()
925 {
926 if (is_null($this->id_last)) {
b6d52898
RB
927 try {
928 $this->id_last = $this->nl->getIssue('last')->id;
929 } catch (MailNotFound $e) {
930 $this->id_last = null;
931 }
ed76a506 932 }
84163d58 933 return $this->id_last;
0337d704 934 }
935
84163d58
RB
936 // }}}
937 // {{{ Edition, articles
938
5f0c028a 939 const ERROR_INVALID_REPLY_TO = 'invalid_reply_to';
84163d58
RB
940 const ERROR_INVALID_SHORTNAME = 'invalid_shortname';
941 const ERROR_INVALID_UFC = 'invalid_ufc';
9ce149f3 942 const ERROR_TOO_LONG_UFC = 'too_long_ufc';
84163d58
RB
943 const ERROR_SQL_SAVE = 'sql_error';
944
945 /** Save the global properties of this NL issue (title&co).
946 */
4882f4a5 947 public function save()
0337d704 948 {
84163d58
RB
949 $errors = array();
950
951 // Fill the list of fields to update
952 $fields = array(
953 'title' => $this->title,
954 'mail_title' => $this->title_mail,
955 'head' => $this->head,
956 'signature' => $this->signature,
957 );
958
5f0c028a
BG
959 if (!empty($this->reply_to) && !isvalid_email($this->reply_to)) {
960 $errors[] = self::ERROR_INVALID_REPLY_TO ;
961 } else {
962 $fields['reply_to'] = $this->reply_to;
963 }
964
84163d58
RB
965 if ($this->isEditable()) {
966 $fields['date'] = $this->date;
967 if (!preg_match('/^[-a-z0-9]+$/i', $this->shortname) || is_numeric($this->shortname)) {
968 $errors[] = self::ERROR_INVALID_SHORTNAME;
969 } else {
970 $fields['short_name'] = $this->shortname;
971 }
972 if ($this->sufb->isValid() || $this->sufb->isEmpty()) {
973 $fields['sufb_json'] = json_encode($this->sufb->export()->dict());
9ce149f3
SJ
974 // If sufb_json is too long to be store, we do not store a truncated json and notify the user.
975 // The limit is LONGTEXT's one, ie 2^32 = 4294967296.
976 if (strlen($fields['sufb_json']) > 4294967295) {
977 $errors[] = self::ERROR_TOO_LONG_UFC;
978 }
84163d58
RB
979 } else {
980 $errors[] = self::ERROR_INVALID_UFC;
981 }
982
983 if ($this->nl->automaticMailingEnabled()) {
984 $fields['send_before'] = ($this->send_before ? $this->send_before : null);
985 }
986 }
987
988 if (count($errors)) {
989 return $errors;
990 }
991 $field_sets = array();
992 foreach ($fields as $key => $value) {
993 $field_sets[] = XDB::format($key . ' = {?}', $value);
994 }
995 XDB::execute('UPDATE newsletter_issues
996 SET ' . implode(', ', $field_sets) . '
997 WHERE id={?}',
998 $this->id);
999 if (XDB::affectedRows()) {
1000 $this->refresh();
1001 } else {
1002 $errors[] = self::ERROR_SQL_SAVE;
1003 }
1004 return $errors;
ed76a506 1005 }
eaf30d86 1006
84163d58
RB
1007 /** Get an article by number
1008 * @p $aid Article ID (among articles of the issue)
1009 * @return A NLArticle object, or null if there is no article by that number
1010 */
4882f4a5 1011 public function getArt($aid)
0337d704 1012 {
84163d58
RB
1013 $this->fetchArticles();
1014
1015 foreach ($this->arts as $category => $artlist) {
1016 if (isset($artlist[$aid])) {
1017 return $artlist[$aid];
0337d704 1018 }
11f44323 1019 }
1020 return null;
0337d704 1021 }
1022
84163d58 1023 /** Save an article
26ba053e 1024 * @p $a A reference to a NLArticle object (will be modified once saved)
84163d58 1025 */
26ba053e 1026 public function saveArticle($a)
0337d704 1027 {
84163d58
RB
1028 $this->fetchArticles();
1029
1030 // Prevent cid to be 0 (use NULL instead)
1031 $a->cid = ($a->cid == 0) ? null : $a->cid;
1032 if ($a->aid >= 0) {
1033 // Article already exists in DB
00ba8a74
SJ
1034 XDB::execute('UPDATE newsletter_art
1035 SET cid = {?}, pos = {?}, title = {?}, body = {?}, append = {?}
1036 WHERE id = {?} AND aid = {?}',
84163d58 1037 $a->cid, $a->pos, $a->title, $a->body, $a->append, $this->id, $a->aid);
11f44323 1038 } else {
84163d58 1039 // New article
00ba8a74
SJ
1040 XDB::startTransaction();
1041 list($aid, $pos) = XDB::fetchOneRow('SELECT MAX(aid) AS aid, MAX(pos) AS pos
1042 FROM newsletter_art AS a
1043 WHERE a.id = {?}',
84163d58
RB
1044 $this->id);
1045 $a->aid = ++$aid;
1046 $a->pos = ($a->pos ? $a->pos : ++$pos);
00ba8a74
SJ
1047 XDB::execute('INSERT INTO newsletter_art (id, aid, cid, pos, title, body, append)
1048 VALUES ({?}, {?}, {?}, {?}, {?}, {?}, {?})',
84163d58
RB
1049 $this->id, $a->aid, $a->cid, $a->pos,
1050 $a->title, $a->body, $a->append);
00ba8a74 1051 XDB::commit();
11f44323 1052 }
84163d58
RB
1053 // Update local ID of article
1054 $this->arts[$a->aid] = $a;
0337d704 1055 }
eaf30d86 1056
84163d58
RB
1057 /** Delete an article by its ID
1058 * @p $aid ID of the article to delete
1059 */
4882f4a5 1060 public function delArticle($aid)
0337d704 1061 {
84163d58
RB
1062 $this->fetchArticles();
1063
1064 XDB::execute('DELETE FROM newsletter_art WHERE id={?} AND aid={?}', $this->id, $aid);
1065 foreach ($this->arts as $key=>$art) {
1066 unset($this->arts[$key][$aid]);
11f44323 1067 }
0337d704 1068 }
1069
84163d58
RB
1070 // }}}
1071 // {{{ Display
1072
1073 /** Retrieve the title of this issue
1074 * @p $mail Whether we want the normal title or the email subject
1075 * @return Title of the issue
1076 */
1077 public function title($mail = false)
4882f4a5 1078 {
84163d58 1079 return $mail ? $this->title_mail : $this->title;
4882f4a5 1080 }
0337d704 1081
84163d58
RB
1082 /** Retrieve the head of this issue
1083 * @p $user User for <dear> customization (may be null: no customization)
1084 * @p $type Either 'text' or 'html'
1085 * @return Formatted head of the issue.
1086 */
1087 public function head($user = null, $type = 'text')
0337d704 1088 {
84163d58
RB
1089 if (is_null($user)) {
1090 return $this->head;
1091 } else {
1092 $head = $this->head;
1093 $head = str_replace(array('<cher>', '<prenom>', '<nom>'),
1094 array(($user->isFemale() ? 'Chère' : 'Cher'), $user->displayName(), ''),
1095 $head);
1096 return format_text($head, $type, 2, 64);
1097 }
eaf30d86 1098 }
0337d704 1099
84163d58
RB
1100 /** Retrieve the formatted signature of this issue.
1101 */
1102 public function signature($type = 'text')
0337d704 1103 {
84163d58
RB
1104 return format_text($this->signature, $type, 2, 64);
1105 }
1106
1107 /** Get the title of a given category
1108 * @p $cid ID of the category to retrieve
1109 * @return Name of the category
1110 */
1111 public function category($cid)
1112 {
1113 return $this->nl->cats[$cid];
1114 }
1115
1116 /** Add required data to the given $page for proper CSS display
1117 * @p $page Smarty object
1118 * @return Either 'true' (if CSS was added to a page) or the raw CSS to add (when $page is null)
1119 */
26ba053e 1120 public function css($page = null)
84163d58
RB
1121 {
1122 if (!is_null($page)) {
1123 $page->addCssLink($this->nl->cssFile());
1124 return true;
1125 } else {
1126 $css = file_get_contents(dirname(__FILE__) . '/../htdocs/css/' . $this->nl->cssFile());
1127 return preg_replace('@/\*.*?\*/@us', '', $css);
1128 }
1129 }
1130
1131 /** Set up a smarty page for a 'text' mode render of the issue
1132 * @p $page Smarty object (using the $this->nl->tplFile() template)
1133 * @p $user User to use when rendering the template
1134 */
26ba053e 1135 public function toText($page, $user)
84163d58
RB
1136 {
1137 $this->fetchArticles();
1138
1139 $this->css($page);
1140 $page->assign('prefix', null);
1141 $page->assign('is_mail', false);
1142 $page->assign('mail_part', 'text');
1143 $page->assign('user', $user);
1144 $page->assign('hash', null);
1145 $this->assignData($page);
eaf30d86
PH
1146 }
1147
84163d58
RB
1148 /** Set up a smarty page for a 'html' mode render of the issue
1149 * @p $page Smarty object (using the $this->nl->tplFile() template)
1150 * @p $user User to use when rendering the template
1151 */
26ba053e 1152 public function toHtml($page, $user)
4882f4a5 1153 {
84163d58
RB
1154 $this->fetchArticles();
1155
1156 $this->css($page);
1157 $page->assign('prefix', $this->nl->prefix() . '/show/' . $this->id());
1158 $page->assign('is_mail', false);
1159 $page->assign('mail_part', 'html');
1160 $page->assign('user', $user);
1161 $page->assign('hash', null);
1162 $this->assignData($page);
0337d704 1163 }
1164
84163d58
RB
1165 /** Set all 'common' data for the page (those which are required for both web and email rendering)
1166 * @p $smarty Smarty object (e.g page) which should be filled
1167 */
26ba053e 1168 protected function assignData($smarty)
4882f4a5 1169 {
84163d58
RB
1170 $this->fetchArticles();
1171
1172 $smarty->assign_by_ref('issue', $this);
1173 $smarty->assign_by_ref('nl', $this->nl);
4882f4a5 1174 }
1175
84163d58
RB
1176 // }}}
1177 // {{{ Mailing
d5524e7b
RB
1178
1179 /** Check whether this issue is empty
1180 * An issue is empty if the email has no title (or the default one), or no articles and an empty head.
1181 */
1182 public function isEmpty()
1183 {
a386dd97 1184 return $this->title_mail == '' || $this->title_mail == 'to be continued' || (count($this->arts) == 0 && strlen($this->head) == 0);
d5524e7b
RB
1185 }
1186
84163d58
RB
1187 /** Retrieve the 'Send before' date, in a clean format.
1188 */
1189 public function getSendBeforeDate()
4882f4a5 1190 {
84163d58 1191 return strftime('%Y-%m-%d', strtotime($this->send_before));
4882f4a5 1192 }
1193
84163d58
RB
1194 /** Retrieve the 'Send before' time (i.e hour), in a clean format.
1195 */
1196 public function getSendBeforeTime()
4882f4a5 1197 {
84163d58 1198 return strtotime($this->send_before);
4882f4a5 1199 }
1200
84163d58
RB
1201 /** Create a hash based on some additional data
1202 * $line Line-specific data (to prevent two hashes generated at the same time to be the same)
1203 */
1204 protected static function createHash($line)
4882f4a5 1205 {
84163d58
RB
1206 $hash = implode(time(), $line) . rand();
1207 $hash = md5($hash);
1208 return $hash;
4882f4a5 1209 }
1210
84163d58
RB
1211 /** Send this issue to the given user, reusing an existing hash if provided.
1212 * @p $user User to whom the issue should be mailed
1213 * @p $hash Optional hash to use in the 'unsubscribe' link; if null, another one will be generated.
1214 */
1215 public function sendTo($user, $hash = null)
4882f4a5 1216 {
84163d58
RB
1217 $this->fetchArticles();
1218
1219 if (is_null($hash)) {
1220 $hash = XDB::fetchOneCell("SELECT hash
1221 FROM newsletter_ins
1222 WHERE uid = {?} AND nlid = {?}",
1223 $user->id(), $this->nl->id);
1224 }
1225 if (is_null($hash)) {
1226 $hash = self::createHash(array($user->displayName(), $user->fullName(),
1227 $user->isFemale(), $user->isEmailFormatHtml(),
1228 rand(), "X.org rulez"));
1229 XDB::execute("UPDATE newsletter_ins as ni
1230 SET ni.hash = {?}
1231 WHERE ni.uid = {?} AND ni.nlid = {?}",
1232 $hash, $user->id(), $this->nl->id);
1233 }
1234
1235 $mailer = new PlMailer($this->nl->tplFile());
1236 $this->assignData($mailer);
1237 $mailer->assign('is_mail', true);
1238 $mailer->assign('user', $user);
1239 $mailer->assign('prefix', null);
1240 $mailer->assign('hash', $hash);
5f0c028a
BG
1241 if (!empty($this->reply_to)) {
1242 $mailer->addHeader('Reply-To', $this->reply_to);
1243 }
84163d58 1244 $mailer->sendTo($user);
4882f4a5 1245 }
84163d58
RB
1246
1247 /** Select a subset of subscribers which should receive the newsletter.
1248 * NL-Specific selections (not yet received, is subscribed) are done when sending.
1249 * @return A PlFilterCondition.
1250 */
1251 protected function getRecipientsUFC()
1252 {
1253 return $this->sufb->getUFC();
1254 }
1255
1256 /** Check whether a given user may see this issue.
1257 * @p $user User whose access should be checked
1258 * @return Whether he may access the issue
1259 */
1260 public function checkUser($user = null)
1261 {
1262 if ($user == null) {
1263 $user = S::user();
1264 }
1265 $uf = new UserFilter($this->getRecipientsUFC());
1266 return $uf->checkUser($user);
1267 }
1268
1269 /** Sent this issue to all valid recipients
1270 * @return Number of issues sent
1271 */
1272 public function sendToAll()
1273 {
1274 $this->fetchArticles();
1275
1276 XDB::execute('UPDATE newsletter_issues
1277 SET state = \'sent\', date=CURDATE()
1278 WHERE id = {?}',
1279 $this->id);
1280
df6d9034 1281 $ufc = new PFC_And($this->getRecipientsUFC(), new UFC_NLSubscribed($this->nl->id, $this->id), new UFC_HasValidEmail());
fdf18de9 1282 $uf = new UserFilter($ufc, array(new UFO_IsAdmin(true), new UFO_Uid()));
00278954 1283 $limit = new PlLimit(self::BATCH_SIZE);
e4502fa1 1284 $global_sent = array();
84163d58
RB
1285
1286 while (true) {
1287 $sent = array();
00278954 1288 $users = $uf->getUsers($limit);
84163d58 1289 if (count($users) == 0) {
e4502fa1 1290 break;
84163d58
RB
1291 }
1292 foreach ($users as $user) {
e4502fa1 1293 if (array_key_exists($user->id(), $global_sent)) {
7e2982c9 1294 Platal::page()->kill('Sending the same newsletter issue ' . $this->id . ' to user ' . $user->id() . ' twice, something must be wrong.');
e4502fa1 1295 }
84163d58 1296 $sent[] = $user->id();
e4502fa1 1297 $global_sent[$user->id()] = true;
84163d58 1298 $this->sendTo($user, $hash);
84163d58
RB
1299 }
1300 XDB::execute("UPDATE newsletter_ins
1301 SET last = {?}
1302 WHERE nlid = {?} AND uid IN {?}", $this->id, $this->nl->id, $sent);
1303
1304 sleep(60);
1305 }
e4502fa1 1306 return count($global_sent);
84163d58
RB
1307 }
1308
1309 // }}}
0337d704 1310}
1311
1312// }}}
1313// {{{ class NLArticle
1314
1315class NLArticle
1316{
84163d58 1317 // Maximum number of lines per article
0fd5be99
SJ
1318 const MAX_LINES_PER_ARTICLE = 8;
1319 const MAX_CHARACTERS_PER_LINE = 68;
84163d58 1320
0337d704 1321 // {{{ properties
eaf30d86 1322
84163d58
RB
1323 public $aid;
1324 public $cid;
1325 public $pos;
1326 public $title;
1327 public $body;
1328 public $append;
0337d704 1329
1330 // }}}
1331 // {{{ constructor
eaf30d86 1332
4882f4a5 1333 function __construct($title='', $body='', $append='', $aid=-1, $cid=0, $pos=0)
0337d704 1334 {
84163d58
RB
1335 $this->body = $body;
1336 $this->title = $title;
1337 $this->append = $append;
1338 $this->aid = $aid;
1339 $this->cid = $cid;
1340 $this->pos = $pos;
0337d704 1341 }
1342
1343 // }}}
1344 // {{{ function title()
1345
4882f4a5 1346 public function title()
84163d58 1347 { return trim($this->title); }
0337d704 1348
1349 // }}}
1350 // {{{ function body()
eaf30d86 1351
4882f4a5 1352 public function body()
84163d58 1353 { return trim($this->body); }
eaf30d86 1354
0337d704 1355 // }}}
1356 // {{{ function append()
eaf30d86 1357
4882f4a5 1358 public function append()
84163d58 1359 { return trim($this->append); }
0337d704 1360
1361 // }}}
1362 // {{{ function toText()
1363
e0e77c5a 1364 public function toText($hash = null, $login = null)
0337d704 1365 {
4882f4a5 1366 $title = '*'.$this->title().'*';
84163d58
RB
1367 $body = MiniWiki::WikiToText($this->body, true);
1368 $app = MiniWiki::WikiToText($this->append, false, 4);
e0e77c5a
FB
1369 $text = trim("$title\n\n$body\n\n$app")."\n";
1370 if (!is_null($hash) && !is_null($login)) {
1371 $text = str_replace('%HASH%', "$hash/$login", $text);
1372 } else {
1373 $text = str_replace('%HASH%', '', $text);
1374 }
1375 return $text;
0337d704 1376 }
1377
1378 // }}}
1379 // {{{ function toHtml()
1380
e0e77c5a 1381 public function toHtml($hash = null, $login = null)
0337d704 1382 {
84163d58
RB
1383 $title = "<h2 class='xorg_nl'><a id='art{$this->aid}'></a>".pl_entities($this->title()).'</h2>';
1384 $body = MiniWiki::WikiToHTML($this->body);
1385 $app = MiniWiki::WikiToHTML($this->append);
eaf30d86 1386
4882f4a5 1387 $art = "$title\n";
1388 $art .= "<div class='art'>\n$body\n";
1389 if ($app) {
0337d704 1390 $art .= "<div class='app'>$app</div>";
1391 }
4882f4a5 1392 $art .= "</div>\n";
e0e77c5a
FB
1393 if (!is_null($hash) && !is_null($login)) {
1394 $art = str_replace('%HASH%', "$hash/$login", $art);
1395 } else {
1396 $art = str_replace('%HASH%', '', $art);
1397 }
eaf30d86 1398
4882f4a5 1399 return $art;
0337d704 1400 }
1401
1402 // }}}
1403 // {{{ function check()
1404
4882f4a5 1405 public function check()
0337d704 1406 {
0fd5be99
SJ
1407 $rest = $this->remain();
1408
1409 return $rest['remaining_lines'] >= 0;
1410 }
1411
1412 // }}}
1413 // {{{ function remain()
1414
1415 public function remain()
1416 {
1417 $text = MiniWiki::WikiToText($this->body);
1418 $array = explode("\n", wordwrap($text, self::MAX_CHARACTERS_PER_LINE));
1419 $lines_count = 0;
1420 foreach ($array as $line) {
1421 if (trim($line) != '') {
1422 ++$lines_count;
0337d704 1423 }
1424 }
0337d704 1425
0fd5be99
SJ
1426 return array(
1427 'remaining_lines' => self::MAX_LINES_PER_ARTICLE - $lines_count,
1428 'remaining_characters_for_last_line' => self::MAX_CHARACTERS_PER_LINE - strlen($array[count($array) - 1])
1429 );
1430 }
0337d704 1431 // }}}
9e2a6a32
SJ
1432 // {{{ function parseUrlsFromArticle()
1433
84163d58 1434 protected function parseUrlsFromArticle()
9e2a6a32
SJ
1435 {
1436 $email_regex = '([a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+)';
1437 $url_regex = '((https?|ftp)://[a-zA-Z0-9._%#+/?=&~-]+)';
1438 $regex = '{' . $email_regex . '|' . $url_regex . '}i';
1439
1440 $matches = array();
1441 $body_matches = array();
1442 if (preg_match_all($regex, $this->body(), $body_matches)) {
1443 $matches = array_merge($matches, $body_matches[0]);
1444 }
1445
1446 $append_matches = array();
1447 if (preg_match_all($regex, $this->append(), $append_matches)) {
1448 $matches = array_merge($matches, $append_matches[0]);
1449 }
1450
1451 return $matches;
1452 }
1453
1454 // }}}
1455 // {{{ function getLinkIps()
1456
955109ba 1457 public function getLinkIps(&$blacklist_host_resolution_count)
9e2a6a32
SJ
1458 {
1459 $matches = $this->parseUrlsFromArticle();
1460 $article_ips = array();
1461
1462 if (!empty($matches)) {
1463 global $globals;
1464
1465 foreach ($matches as $match) {
1466 $host = parse_url($match, PHP_URL_HOST);
1467 if ($host == '') {
1468 list(, $host) = explode('@', $match);
1469 }
1470
955109ba 1471 if ($blacklist_host_resolution_count >= $globals->mail->blacklist_host_resolution_limit) {
9e2a6a32
SJ
1472 break;
1473 }
1474
955109ba 1475 if (!preg_match('/^(' . str_replace(' ', '|', $globals->mail->domain_whitelist) . ')$/i', $host)) {
9e2a6a32 1476 $article_ips = array_merge($article_ips, array(gethostbyname($host) => $host));
955109ba 1477 ++$blacklist_host_resolution_count;
9e2a6a32
SJ
1478 }
1479 }
1480 }
1481
1482 return $article_ips;
1483 }
1484
1485 // }}}
0337d704 1486}
1487
1488// }}}
0337d704 1489
84163d58
RB
1490// {{{ Functions
1491
1492function format_text($input, $format, $indent = 0, $width = 68)
1493{
1494 if ($format == 'text') {
1495 return MiniWiki::WikiToText($input, true, $indent, $width, "title");
1496 }
1497 return MiniWiki::WikiToHTML($input, "title");
1498}
1499
1500// function enriched_to_text($input,$html=false,$just=false,$indent=0,$width=68)
1501
1502// }}}
1503
a7de4ef7 1504// vim:set et sw=4 sts=4 sws=4 enc=utf-8:
0337d704 1505?>