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