2 /***************************************************************************
3 * Copyright (C) 2003-2011 Polytechnique.org *
4 * http://opensource.polytechnique.org/ *
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. *
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. *
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 *
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 ***************************************************************************/
22 // {{{ class MailNotFound
24 class MailNotFound
extends Exception
{
29 // {{{ class NewsLetter
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
40 protected $custom_css = false
;
42 // Base name to use instead of the group short name for NLs without a custom CSS
43 const FORMAT_DEFAULT_GROUP
= 'default';
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';
50 // {{{ Constructor, NewsLetter retrieval (forGroup, getAll)
52 public function __construct($id)
55 $res = XDB
::query('SELECT nls.group_id, g.diminutif AS group_name,
56 nls.name AS nl_name, nls.custom_css, nls.criteria
57 FROM newsletters AS nls
58 LEFT JOIN groups AS g ON (nls.group_id = g.id)
61 if (!$res->numRows()) {
62 throw new MailNotFound();
65 $data = $res->fetchOneAssoc();
67 $this->group_id
= $data['group_id'];
68 $this->group
= $data['group_name'];
69 $this->name
= $data['nl_name'];
70 $this->custom_css
= $data['custom_css'];
71 $this->criteria
= new PlFlagSet($data['criteria']);
73 // Load the categories
79 while (list($cid, $title) = $res->next()) {
80 $this->cats
[$cid] = $title;
84 /** Retrieve the NL associated with a given group.
85 * @p $group Short name of the group
86 * @return A NewsLetter object, or null if the group doesn't have a NL.
88 public static function forGroup($group)
90 $res = XDB
::query('SELECT nls.id
91 FROM newsletters AS nls
92 LEFT JOIN groups AS g ON (nls.group_id = g.id)
93 WHERE g.diminutif = {?}', $group);
94 if (!$res->numRows()) {
97 return new NewsLetter($res->fetchOneCell());
100 /** Retrieve all newsletters
101 * @return An array of $id => NewsLetter objects
103 public static function getAll()
105 $res = XDB
::query('SELECT id
108 foreach ($res->fetchColumn() as $id) {
109 $nls[$id] = new NewsLetter($id);
115 // {{{ Issue retrieval
117 /** Retrieve all issues which should be sent
118 * @return An array of NLIssue objects to send (i.e state = 'new' and send_before <= today)
120 public static function getIssuesToSend()
122 $res = XDB
::query('SELECT id
123 FROM newsletter_issues
124 WHERE state = \'pending\' AND send_before <= NOW()');
126 foreach ($res->fetchColumn() as $id) {
127 $issues[$id] = new NLIssue($id);
132 /** Retrieve a given issue of this NewsLetter
133 * @p $name Name or ID of the issue to retrieve.
134 * @return A NLIssue object.
136 * $name may be either a short_name, an ID or the special value 'last' which
137 * selects the latest sent NL.
138 * If $name is null, this will retrieve the current pending NL.
140 public function getIssue($name = null
, $only_sent = true
)
143 if ($name == 'last') {
145 $where = 'state = \'sent\' AND ';
149 $res = XDB
::query('SELECT MAX(id)
150 FROM newsletter_issues
151 WHERE ' . $where . ' nlid = {?}',
154 $res = XDB
::query('SELECT id
155 FROM newsletter_issues
156 WHERE nlid = {?} AND (id = {?} OR short_name = {?})',
157 $this->id
, $name, $name);
159 if (!$res->numRows()) {
160 throw new MailNotFound();
162 $id = $res->fetchOneCell();
164 $query = XDB
::format('SELECT id
165 FROM newsletter_issues
166 WHERE nlid = {?} AND state = \'new\'
167 ORDER BY id DESC', $this->id
);
168 $res = XDB
::query($query);
169 if ($res->numRows()) {
170 $id = $res->fetchOneCell();
172 // Create a new, empty issue, and return it
173 $id = $this->createPending();
177 return new NLIssue($id, &$this);
180 /** Create a new, empty, pending newsletter issue
181 * @p $nlid The id of the NL for which a new pending issue should be created.
182 * @return Id of the newly created issue.
184 public function createPending()
186 XDB
::execute('INSERT INTO newsletter_issues
187 SET nlid = {?}, state=\'new\', date=NOW(),
188 title=\'to be continued\',
189 mail_title=\'to be continued\'',
191 return XDB
::insertId();
194 /** Return all sent issues of this newsletter.
195 * @return An array of (id => NLIssue)
197 public function listSentIssues($check_user = false
, $user = null
)
199 if ($check_user && $user == null
) {
203 $res = XDB
::query('SELECT id
204 FROM newsletter_issues
205 WHERE nlid = {?} AND state = \'sent\'
206 ORDER BY date DESC', $this->id
);
208 foreach ($res->fetchColumn() as $id) {
209 $issue = new NLIssue($id, $this, false
);
210 if (!$check_user ||
$issue->checkUser($user)) {
211 $issues[$id] = $issue;
217 /** Return all issues of this newsletter, including invalid and sent.
218 * @return An array of (id => NLIssue)
220 public function listAllIssues()
222 $res = XDB
::query('SELECT id
223 FROM newsletter_issues
225 ORDER BY FIELD(state, \'pending\', \'new\') DESC, date DESC', $this->id
);
227 foreach ($res->fetchColumn() as $id) {
228 $issues[$id] = new NLIssue($id, $this, false
);
233 /** Return the latest pending issue of the newsletter.
234 * @p $create Whether to create an empty issue if no pending issue exist.
235 * @return Either null, or a NL object.
237 public function getPendingIssue($create = false
)
239 $res = XDB
::query('SELECT MAX(id)
240 FROM newsletter_issues
241 WHERE nlid = {?} AND state = \'new\'',
243 $id = $res->fetchOneCell();
245 return new NLIssue($id, $this);
246 } else if ($create) {
247 $id = $this->createPending();
248 return new NLIssue($id, $this);
255 // {{{ Subscription related function
257 /** Unsubscribe a user from this newsletter
258 * @p $uid UID to unsubscribe from the newsletter; if null, use current user.
259 * @p $hash True if the uid is actually a hash.
260 * @return True if the user was successfully unsubscribed.
262 public function unsubscribe($uid = null
, $hash = false
)
264 if (is_null($uid) && $hash) {
265 // Unable to unsubscribe from an empty hash
268 $user = is_null($uid) ? S
::user()->id() : $uid;
269 $field = $hash ?
'hash' : 'uid';
270 $res = XDB
::query('SELECT uid
272 WHERE nlid = {?} AND ' . $field . ' = {?}',
274 if (!$res->numRows()) {
275 // No subscribed user with that UID/hash
278 $user = $res->fetchOneCell();
280 XDB
::execute('DELETE FROM newsletter_ins
281 WHERE nlid = {?} AND uid = {?}',
286 /** Subscribe a user to a newsletter
287 * @p $user User to subscribe to the newsletter; if null, use current user.
289 public function subscribe($user = null
)
291 if (is_null($user)) {
294 if (self
::maySubscribe($user)) {
295 XDB
::execute('INSERT IGNORE INTO newsletter_ins (nlid, uid, last, hash)
296 VALUES ({?}, {?}, NULL, hash)',
297 $this->id
, $user->id());
301 /** Retrieve subscription state of a user
302 * @p $user Target user; if null, use current user.
303 * @return Boolean: true if the user has subscribed to the NL.
305 public function subscriptionState($user = null
)
307 if (is_null($user)) {
310 $res = XDB
::query('SELECT 1
312 WHERE nlid = {?} AND uid = {?}',
313 $this->id
, $user->id());
314 return ($res->numRows() == 1);
317 /** Get the count of subscribers to the NL.
318 * @return Number of subscribers.
320 public function subscriberCount()
322 return XDB
::fetchOneCell('SELECT COUNT(uid)
324 WHERE nlid = {?}', $this->id
);
327 /** Get the number of subscribers to the NL whose last received mailing was $last.
328 * @p $last ID of the issue for which subscribers should be counted.
329 * @return Number of subscribers
331 public function subscriberCountForLast($last)
333 return XDB
::fetchOneCell('SELECT COUNT(uid)
335 WHERE nlid = {?} AND last = {?}', $this->id
, $last);
338 /** Retrieve the list of newsletters a user has subscribed to
339 * @p $user User whose subscriptions should be retrieved (if null, use session user).
340 * @return Array of newsletter IDs
342 public static function getUserSubscriptions($user = null
)
344 if (is_null($user)) {
347 $res = XDB
::query('SELECT nlid
351 return $res->fetchColumn();
354 /** Retrieve the UserFilterBuilder for subscribers to this NL.
355 * This is the place where NL-specific filters may be allowed or prevented.
356 * @p $envprefix Prefix to use for env fields (cf. UserFilterBuilder)
357 * @return A UserFilterBuilder object using the given env prefix
359 public function getSubscribersUFB($envprefix = '')
361 require_once 'ufbuilder.inc.php';
362 return new UFB_NewsLetter($this->criteria
, $envprefix);
366 // {{{ Permissions related functions
368 /** For later use: check whether a given user may subscribe to this newsletter.
369 * @p $user User whose access should be checked
370 * @return Boolean: whether the user may subscribe to the NL.
372 public function maySubscribe($user = null
)
377 /** Whether a given user may edit this newsletter
378 * @p $uid UID of the user whose perms should be checked (if null, use current user)
379 * @return Boolean: whether the user may edit the NL
381 public function mayEdit($user = null
)
383 if (is_null($user)) {
386 if ($user->checkPerms('admin')) {
389 $res = XDB
::query('SELECT perms
391 WHERE asso_id = {?} AND uid = {?}',
392 $this->group_id
, $user->id());
393 return ($res->numRows() && $res->fetchOneCell() == 'admin');
396 /** Whether a given user may submit articles to this newsletter using X.org validation system
397 * @p $user User whose access should be checked (if null, use current user)
398 * @return Boolean: whether the user may submit articles
400 public function maySubmit($user = null
)
402 // Submission of new articles is only enabled for the X.org NL (and forbidden when viewing issues on X.net)
403 return ($this->group
== self
::GROUP_XORG
&& !isset($GLOBALS['IS_XNET_SITE']));
407 // {{{ Display-related functions: cssFile, tplFile, prefix, admin_prefix, admin_links_enabled, automatic_mailings_enabled
409 /** Get the name of the css file used to display this newsletter.
411 public function cssFile()
413 if ($this->custom_css
) {
414 $base = $this->group
;
416 $base = self
::FORMAT_DEFAULT_GROUP
;
418 return 'nl.' . $base . '.css';
421 /** Get the name of the template file used to display this newsletter.
423 public function tplFile()
425 if ($this->custom_css
) {
426 $base = $this->group
;
428 $base = self
::FORMAT_DEFAULT_GROUP
;
430 return 'newsletter/nl.' . $base . '.mail.tpl';
433 /** Get the prefix leading to the page for this NL
434 * Only X.org / AX / X groups may be seen on X.org.
436 public function prefix()
438 if (!empty($GLOBALS['IS_XNET_SITE'])) {
439 return $this->group
. '/nl';
441 switch ($this->group
) {
442 case self
::GROUP_XORG
:
449 // Don't display groups NLs on X.org
454 /** Get the prefix to use for all 'admin' pages of this NL.
456 public function adminPrefix()
458 if (!empty($GLOBALS['IS_XNET_SITE'])) {
459 return $this->group
. '/admin/nl';
461 switch ($this->group
) {
462 case self
::GROUP_XORG
:
463 return 'admin/newsletter';
467 return 'epletter/admin';
469 // Don't display groups NLs on X.org
474 /** Hack used to remove "admin" links on X.org page on X.net
475 * The 'admin' links are enabled for all pages, except for X.org when accessing NL through X.net
477 public function adminLinksEnabled()
479 return ($this->group
!= self
::GROUP_XORG ||
!isset($GLOBALS['IS_XNET_SITE']));
482 /** Automatic mailings are disabled for X.org NL.
484 public function automaticMailingEnabled()
486 return $this->group
!= self
::GROUP_XORG
;
489 public function hasCustomCss()
491 return $this->custom_css
;
501 // A NLIssue is an issue of a given NewsLetter
504 protected $nlid; // Id of the newsletter
506 const STATE_NEW
= 'new'; // New, currently being edited
507 const STATE_PENDING
= 'pending'; // Ready for mailing
508 const STATE_SENT
= 'sent'; // Sent
510 public $nl; // Related NL
512 public $id; // Id of this issue of the newsletter
513 public $shortname; // Shortname for this issue
514 public $title; // Title of this issue
515 public $title_mail; // Title of the email
516 public $state; // State of the issue (one of the STATE_ values)
517 public $sufb; // Environment to use to generate the UFC through an UserFilterBuilder
519 public $date; // Date at which this issue was sent
520 public $send_before; // Date at which issue should be sent
521 public $head; // Foreword of the issue (or body for letters with no articles)
522 public $signature; // Signature of the letter
523 public $arts = array(); // Articles of the issue
525 const BATCH_SIZE
= 60; // Number of emails to send every minute.
527 // {{{ Constructor, id-related functions
529 /** Build a NewsLetter.
530 * @p $id: ID of the issue (unique among all newsletters)
531 * @p $nl: Optional argument containing an already built NewsLetter object.
533 function __construct($id, $nl = null
, $fetch_articles = true
)
535 return $this->fetch($id, $nl, $fetch_articles);
538 protected function refresh()
540 return $this->fetch($this->id
, $this->nl
, false
);
543 protected function fetch($id, $nl = null
, $fetch_articles = true
)
546 $res = XDB
::query('SELECT nlid, short_name, date, send_before, state, sufb_json,
547 title, mail_title, head, signature
548 FROM newsletter_issues
551 if (!$res->numRows()) {
552 throw new MailNotFound();
554 $issue = $res->fetchOneAssoc();
555 if ($nl && $nl->id
== $issue['nlid']) {
558 $this->nl
= new NewsLetter($issue['nlid']);
561 $this->shortname
= $issue['short_name'];
562 $this->date
= $issue['date'];
563 $this->send_before
= $issue['send_before'];
564 $this->state
= $issue['state'];
565 $this->title
= $issue['title'];
566 $this->title_mail
= $issue['mail_title'];
567 $this->head
= $issue['head'];
568 $this->signature
= $issue['signature'];
569 $this->sufb
= $this->importJSonStoredUFB($issue['sufb_json']);
571 if ($fetch_articles) {
572 $this->fetchArticles();
576 protected function fetchArticles($force = false
)
578 if (count($this->arts
) && !$force) {
584 'SELECT a.title, a.body, a.append, a.aid, a.cid, a.pos
585 FROM newsletter_art AS a
586 INNER JOIN newsletter_issues AS ni USING(id)
587 LEFT JOIN newsletter_cat AS c ON (a.cid = c.cid)
589 ORDER BY c.pos, a.pos',
591 while (list($title, $body, $append, $aid, $cid, $pos) = $res->next()) {
592 $this->arts
[$cid][$aid] = new NLArticle($title, $body, $append, $aid, $cid, $pos);
596 protected function importJSonStoredUFB($json = null
)
598 require_once 'ufbuilder.inc.php';
599 $ufb = $this->nl
->getSubscribersUFB();
600 if (is_null($json)) {
601 return new StoredUserFilterBuilder($ufb, new PFC_True());
603 $export = json_decode($json, true
);
604 if (is_null($export)) {
605 PlErrorReport
::report("Invalid json while reading NL {$this->nlid}, issue {$this->id}: failed to import '''{$json}'''.");
606 return new StoredUserFilterBuilder($ufb, new PFC_True());
608 $sufb = new StoredUserFilterBuilder($ufb);
609 $sufb->fillFromExport($export);
613 protected function exportStoredUFBAsJSon()
615 return json_encode($this->sufb
->export());
620 return is_null($this->shortname
) ?
$this->id
: $this->shortname
;
623 protected function selectId($where)
625 $res = XDB
::query("SELECT IFNULL(ni.short_name, ni.id)
626 FROM newsletter_issues AS ni
627 WHERE ni.state != 'new' AND ni.nlid = {?} AND ${where}
628 LIMIT 1", $this->nl
->id
);
629 if ($res->numRows() != 1) {
632 return $res->fetchOneCell();
635 /** Delete this issue
636 * @return True if the issue could be deleted, false otherwise.
637 * Related articles will be deleted through cascading FKs.
638 * If this issue was the last issue for at least one subscriber, the deletion will be aborted.
640 public function delete()
642 if ($this->state
== self
::STATE_NEW
) {
643 $res = XDB
::query('SELECT COUNT(*)
645 WHERE last = {?}', $this->id
);
646 if ($res->fetchOneCell() > 0) {
650 return XDB
::execute('DELETE FROM newsletter_issues
651 WHERE id = {?}', $this->id
);
657 /** Schedule a mailing of this NL
658 * If the 'send_before' field was NULL, it is set to the current time.
659 * @return Boolean Whether the date could be set (false if trying to schedule an already sent NL)
661 public function scheduleMailing()
663 if ($this->state
== self
::STATE_NEW
) {
664 $success = XDB
::execute('UPDATE newsletter_issues
665 SET state = \'pending\', send_before = IFNULL(send_before, NOW())
677 /** Cancel the scheduled mailing of this NL
678 * @return Boolean: whether the mailing could be cancelled.
680 public function cancelMailing()
682 if ($this->state
== self
::STATE_PENDING
) {
683 $success = XDB
::execute('UPDATE newsletter_issues
684 SET send_before = NULL, state = \'new\'
685 WHERE id = {?}', $this->id
);
695 /** Helper function for smarty templates: is this issue editable ?
697 public function isEditable()
699 return $this->state
== self
::STATE_NEW
;
702 /** Helper function for smarty templates: is the mailing of this issue scheduled ?
704 public function isPending()
706 return $this->state
== self
::STATE_PENDING
;
709 /** Helper function for smarty templates: has this issue been sent ?
711 public function isSent()
713 return $this->state
== self
::STATE_SENT
;
719 private $id_prev = null
;
720 private $id_next = null
;
721 private $id_last = null
;
723 /** Retrieve ID of the previous issue
724 * That value, once fetched, is cached in the private $id_prev variable.
725 * @return ID of the previous issue.
727 public function prev()
729 if (is_null($this->id_prev
)) {
730 $this->id_prev
= $this->selectId(XDB
::format("ni.id < {?} ORDER BY ni.id DESC", $this->id
));
732 return $this->id_prev
;
735 /** Retrieve ID of the following issue
736 * That value, once fetched, is cached in the private $id_next variable.
737 * @return ID of the following issue.
739 public function next()
741 if (is_null($this->id_next
)) {
742 $this->id_next
= $this->selectId(XDB
::format("ni.id > {?} ORDER BY ni.id", $this->id
));
744 return $this->id_next
;
747 /** Retrieve ID of the last issue
748 * That value, once fetched, is cached in the private $id_last variable.
749 * @return ID of the last issue.
751 public function last()
753 if (is_null($this->id_last
)) {
754 $this->id_last
= $this->nl
->getIssue('last')->id
;
756 return $this->id_last
;
760 // {{{ Edition, articles
762 const ERROR_INVALID_SHORTNAME
= 'invalid_shortname';
763 const ERROR_INVALID_UFC
= 'invalid_ufc';
764 const ERROR_SQL_SAVE
= 'sql_error';
766 /** Save the global properties of this NL issue (title&co).
768 public function save()
772 // Fill the list of fields to update
774 'title' => $this->title
,
775 'mail_title' => $this->title_mail
,
776 'head' => $this->head
,
777 'signature' => $this->signature
,
780 if ($this->isEditable()) {
781 $fields['date'] = $this->date
;
782 if (!preg_match('/^[-a-z0-9]+$/i', $this->shortname
) ||
is_numeric($this->shortname
)) {
783 $errors[] = self
::ERROR_INVALID_SHORTNAME
;
785 $fields['short_name'] = $this->shortname
;
787 if ($this->sufb
->isValid() ||
$this->sufb
->isEmpty()) {
788 $fields['sufb_json'] = json_encode($this->sufb
->export()->dict());
790 $errors[] = self
::ERROR_INVALID_UFC
;
793 if ($this->nl
->automaticMailingEnabled()) {
794 $fields['send_before'] = ($this->send_before ?
$this->send_before
: null
);
798 if (count($errors)) {
801 $field_sets = array();
802 foreach ($fields as $key => $value) {
803 $field_sets[] = XDB
::format($key . ' = {?}', $value);
805 XDB
::execute('UPDATE newsletter_issues
806 SET ' . implode(', ', $field_sets) . '
809 if (XDB
::affectedRows()) {
812 $errors[] = self
::ERROR_SQL_SAVE
;
817 /** Get an article by number
818 * @p $aid Article ID (among articles of the issue)
819 * @return A NLArticle object, or null if there is no article by that number
821 public function getArt($aid)
823 $this->fetchArticles();
825 foreach ($this->arts
as $category => $artlist) {
826 if (isset($artlist[$aid])) {
827 return $artlist[$aid];
834 * @p &$a A reference to a NLArticle object (will be modified once saved)
836 public function saveArticle(&$a)
838 $this->fetchArticles();
840 // Prevent cid to be 0 (use NULL instead)
841 $a->cid
= ($a->cid
== 0) ? null
: $a->cid
;
843 // Article already exists in DB
844 XDB
::execute('UPDATE newsletter_art
845 SET cid = {?}, pos = {?}, title = {?}, body = {?}, append = {?}
846 WHERE id = {?} AND aid = {?}',
847 $a->cid
, $a->pos
, $a->title
, $a->body
, $a->append
, $this->id
, $a->aid
);
850 XDB
::startTransaction();
851 list($aid, $pos) = XDB
::fetchOneRow('SELECT MAX(aid) AS aid, MAX(pos) AS pos
852 FROM newsletter_art AS a
856 $a->pos
= ($a->pos ?
$a->pos
: ++
$pos);
857 XDB
::execute('INSERT INTO newsletter_art (id, aid, cid, pos, title, body, append)
858 VALUES ({?}, {?}, {?}, {?}, {?}, {?}, {?})',
859 $this->id
, $a->aid
, $a->cid
, $a->pos
,
860 $a->title
, $a->body
, $a->append
);
863 // Update local ID of article
864 $this->arts
[$a->aid
] = $a;
867 /** Delete an article by its ID
868 * @p $aid ID of the article to delete
870 public function delArticle($aid)
872 $this->fetchArticles();
874 XDB
::execute('DELETE FROM newsletter_art WHERE id={?} AND aid={?}', $this->id
, $aid);
875 foreach ($this->arts
as $key=>$art) {
876 unset($this->arts
[$key][$aid]);
883 /** Retrieve the title of this issue
884 * @p $mail Whether we want the normal title or the email subject
885 * @return Title of the issue
887 public function title($mail = false
)
889 return $mail ?
$this->title_mail
: $this->title
;
892 /** Retrieve the head of this issue
893 * @p $user User for <dear> customization (may be null: no customization)
894 * @p $type Either 'text' or 'html'
895 * @return Formatted head of the issue.
897 public function head($user = null
, $type = 'text')
899 if (is_null($user)) {
903 $head = str_replace(array('<cher>', '<prenom>', '<nom>'),
904 array(($user->isFemale() ?
'Chère' : 'Cher'), $user->displayName(), ''),
906 return format_text($head, $type, 2, 64);
910 /** Retrieve the formatted signature of this issue.
912 public function signature($type = 'text')
914 return format_text($this->signature
, $type, 2, 64);
917 /** Get the title of a given category
918 * @p $cid ID of the category to retrieve
919 * @return Name of the category
921 public function category($cid)
923 return $this->nl
->cats
[$cid];
926 /** Add required data to the given $page for proper CSS display
927 * @p $page Smarty object
928 * @return Either 'true' (if CSS was added to a page) or the raw CSS to add (when $page is null)
930 public function css(&$page = null
)
932 if (!is_null($page)) {
933 $page->addCssLink($this->nl
->cssFile());
936 $css = file_get_contents(dirname(__FILE__
) . '/../htdocs/css/' . $this->nl
->cssFile());
937 return preg_replace('@/\*.*?\*/@us', '', $css);
941 /** Set up a smarty page for a 'text' mode render of the issue
942 * @p $page Smarty object (using the $this->nl->tplFile() template)
943 * @p $user User to use when rendering the template
945 public function toText(&$page, $user)
947 $this->fetchArticles();
950 $page->assign('prefix', null
);
951 $page->assign('is_mail', false
);
952 $page->assign('mail_part', 'text');
953 $page->assign('user', $user);
954 $page->assign('hash', null
);
955 $this->assignData($page);
958 /** Set up a smarty page for a 'html' mode render of the issue
959 * @p $page Smarty object (using the $this->nl->tplFile() template)
960 * @p $user User to use when rendering the template
962 public function toHtml(&$page, $user)
964 $this->fetchArticles();
967 $page->assign('prefix', $this->nl
->prefix() . '/show/' . $this->id());
968 $page->assign('is_mail', false
);
969 $page->assign('mail_part', 'html');
970 $page->assign('user', $user);
971 $page->assign('hash', null
);
972 $this->assignData($page);
975 /** Set all 'common' data for the page (those which are required for both web and email rendering)
976 * @p $smarty Smarty object (e.g page) which should be filled
978 protected function assignData(&$smarty)
980 $this->fetchArticles();
982 $smarty->assign_by_ref('issue', $this);
983 $smarty->assign_by_ref('nl', $this->nl
);
989 /** Retrieve the 'Send before' date, in a clean format.
991 public function getSendBeforeDate()
993 return strftime('%Y-%m-%d', strtotime($this->send_before
));
996 /** Retrieve the 'Send before' time (i.e hour), in a clean format.
998 public function getSendBeforeTime()
1000 return strtotime($this->send_before
);
1003 /** Create a hash based on some additional data
1004 * $line Line-specific data (to prevent two hashes generated at the same time to be the same)
1006 protected static function createHash($line)
1008 $hash = implode(time(), $line) . rand();
1013 /** Send this issue to the given user, reusing an existing hash if provided.
1014 * @p $user User to whom the issue should be mailed
1015 * @p $hash Optional hash to use in the 'unsubscribe' link; if null, another one will be generated.
1017 public function sendTo($user, $hash = null
)
1019 $this->fetchArticles();
1021 if (is_null($hash)) {
1022 $hash = XDB
::fetchOneCell("SELECT hash
1024 WHERE uid = {?} AND nlid = {?}",
1025 $user->id(), $this->nl
->id
);
1027 if (is_null($hash)) {
1028 $hash = self
::createHash(array($user->displayName(), $user->fullName(),
1029 $user->isFemale(), $user->isEmailFormatHtml(),
1030 rand(), "X.org rulez"));
1031 XDB
::execute("UPDATE newsletter_ins as ni
1033 WHERE ni.uid = {?} AND ni.nlid = {?}",
1034 $hash, $user->id(), $this->nl
->id
);
1037 $mailer = new PlMailer($this->nl
->tplFile());
1038 $this->assignData($mailer);
1039 $mailer->assign('is_mail', true
);
1040 $mailer->assign('user', $user);
1041 $mailer->assign('prefix', null
);
1042 $mailer->assign('hash', $hash);
1043 $mailer->sendTo($user);
1046 /** Select a subset of subscribers which should receive the newsletter.
1047 * NL-Specific selections (not yet received, is subscribed) are done when sending.
1048 * @return A PlFilterCondition.
1050 protected function getRecipientsUFC()
1052 return $this->sufb
->getUFC();
1055 /** Check whether a given user may see this issue.
1056 * @p $user User whose access should be checked
1057 * @return Whether he may access the issue
1059 public function checkUser($user = null
)
1061 if ($user == null
) {
1064 $uf = new UserFilter($this->getRecipientsUFC());
1065 return $uf->checkUser($user);
1068 /** Sent this issue to all valid recipients
1069 * @return Number of issues sent
1071 public function sendToAll()
1073 $this->fetchArticles();
1075 XDB
::execute('UPDATE newsletter_issues
1076 SET state = \'sent\', date=CURDATE()
1080 $ufc = new PFC_And($this->getRecipientsUFC(), new UFC_NLSubscribed($this->nl
->id
, $this->id
), new UFC_HasEmailRedirect());
1082 $uf = new UserFilter($ufc);
1083 $limit = new PlLimit(self
::BATCH_SIZE
);
1087 $users = $uf->getUsers($limit);
1088 if (count($users) == 0) {
1089 return $emailsCount;
1091 foreach ($users as $user) {
1092 $sent[] = $user->id();
1093 $this->sendTo($user, $hash);
1096 XDB
::execute("UPDATE newsletter_ins
1098 WHERE nlid = {?} AND uid IN {?}", $this->id
, $this->nl
->id
, $sent);
1102 return $emailsCount;
1109 // {{{ class NLArticle
1113 // Maximum number of lines per article
1114 const MAX_LINES_PER_ARTICLE
= 9;
1128 function __construct($title='', $body='', $append='', $aid=-1, $cid=0, $pos=0)
1130 $this->body
= $body;
1131 $this->title
= $title;
1132 $this->append
= $append;
1139 // {{{ function title()
1141 public function title()
1142 { return trim($this->title
); }
1145 // {{{ function body()
1147 public function body()
1148 { return trim($this->body
); }
1151 // {{{ function append()
1153 public function append()
1154 { return trim($this->append
); }
1157 // {{{ function toText()
1159 public function toText($hash = null
, $login = null
)
1161 $title = '*'.$this->title().'*';
1162 $body = MiniWiki
::WikiToText($this->body
, true
);
1163 $app = MiniWiki
::WikiToText($this->append
, false
, 4);
1164 $text = trim("$title\n\n$body\n\n$app")."\n";
1165 if (!is_null($hash) && !is_null($login)) {
1166 $text = str_replace('%HASH%', "$hash/$login", $text);
1168 $text = str_replace('%HASH%', '', $text);
1174 // {{{ function toHtml()
1176 public function toHtml($hash = null
, $login = null
)
1178 $title = "<h2 class='xorg_nl'><a id='art{$this->aid}'></a>".pl_entities($this->title()).'</h2>';
1179 $body = MiniWiki
::WikiToHTML($this->body
);
1180 $app = MiniWiki
::WikiToHTML($this->append
);
1183 $art .= "<div class='art'>\n$body\n";
1185 $art .= "<div class='app'>$app</div>";
1188 if (!is_null($hash) && !is_null($login)) {
1189 $art = str_replace('%HASH%', "$hash/$login", $art);
1191 $art = str_replace('%HASH%', '', $art);
1198 // {{{ function check()
1200 public function check()
1202 $text = MiniWiki
::WikiToText($this->body
);
1203 $arr = explode("\n",wordwrap($text,68));
1205 foreach ($arr as $line) {
1210 return $c < self
::MAX_LINES_PER_ARTICLE
;
1214 // {{{ function parseUrlsFromArticle()
1216 protected function parseUrlsFromArticle()
1218 $email_regex = '([a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+)';
1219 $url_regex = '((https?|ftp)://[a-zA-Z0-9._%#+/?=&~-]+)';
1220 $regex = '{' . $email_regex . '|' . $url_regex . '}i';
1223 $body_matches = array();
1224 if (preg_match_all($regex, $this->body(), $body_matches)) {
1225 $matches = array_merge($matches, $body_matches[0]);
1228 $append_matches = array();
1229 if (preg_match_all($regex, $this->append(), $append_matches)) {
1230 $matches = array_merge($matches, $append_matches[0]);
1237 // {{{ function getLinkIps()
1239 public function getLinkIps(&$blacklist_host_resolution_count)
1241 $matches = $this->parseUrlsFromArticle();
1242 $article_ips = array();
1244 if (!empty($matches)) {
1247 foreach ($matches as $match) {
1248 $host = parse_url($match, PHP_URL_HOST
);
1250 list(, $host) = explode('@', $match);
1253 if ($blacklist_host_resolution_count >= $globals->mail
->blacklist_host_resolution_limit
) {
1257 if (!preg_match('/^(' . str_replace(' ', '|', $globals->mail
->domain_whitelist
) . ')$/i', $host)) {
1258 $article_ips = array_merge($article_ips, array(gethostbyname($host) => $host));
1259 ++
$blacklist_host_resolution_count;
1264 return $article_ips;
1274 function format_text($input, $format, $indent = 0, $width = 68)
1276 if ($format == 'text') {
1277 return MiniWiki
::WikiToText($input, true
, $indent, $width, "title");
1279 return MiniWiki
::WikiToHTML($input, "title");
1282 // function enriched_to_text($input,$html=false,$just=false,$indent=0,$width=68)
1286 // vim:set et sw=4 sts=4 sws=4 enc=utf-8: