X-Git-Url: http://git.polytechnique.org/?a=blobdiff_plain;f=include%2Fnewsletter.inc.php;h=a51aeae68a207905c4864c3fc02e9d9013c89b10;hb=4533e245b8c97acd1bfe12cfa01547e8eac9761a;hp=3df810f731dece62e82e6a751d48233f35ebf8dc;hpb=d5524e7bd1216a4e0b8ea084f41e7e7df801deef;p=platal.git diff --git a/include/newsletter.inc.php b/include/newsletter.inc.php index 3df810f..a51aeae 100644 --- a/include/newsletter.inc.php +++ b/include/newsletter.inc.php @@ -46,6 +46,12 @@ class NewsLetter const GROUP_XORG = 'Polytechnique.org'; const GROUP_AX = 'AX'; const GROUP_EP = 'Ecole'; + const GROUP_FX = 'FX'; + + // Searches on mutiple fields + const SEARCH_ALL = 'all'; + const SEARCH_TITLE = 'title'; + // {{{ Constructor, NewsLetter retrieval (forGroup, getAll) @@ -100,15 +106,13 @@ class NewsLetter /** Retrieve all newsletters * @return An array of $id => NewsLetter objects */ - public static function getAll() + public static function getAll($sort = 'id', $order = 'ASC') { - $res = XDB::query('SELECT id - FROM newsletters'); - $nls = array(); - foreach ($res->fetchColumn() as $id) { - $nls[$id] = new NewsLetter($id); - } - return $nls; + $res = XDB::fetchAllAssoc('SELECT n.id, g.nom AS group_name, n.name, n.custom_css, n.criteria, g.diminutif AS group_link + FROM newsletters AS n + INNER JOIN groups AS g ON (n.group_id = g.id) + ORDER BY ' . $sort . ' ' . $order); + return $res; } // }}} @@ -251,6 +255,63 @@ class NewsLetter } } + /** Returns a list of either issues or articles corresponding to the search. + * @p $search The searched pattern. + * @p $field The fields where to search, if none given, search in all possible fields. + * @return The list of object found. + */ + public function issueSearch($search, $field, $user) + { + $search = XDB::formatWildcards(XDB::WILDCARD_CONTAINS, $search); + if ($field == self::SEARCH_ALL) { + $where = '(title ' . $search . ' OR mail_title ' . $search . ' OR head ' . $search . ' OR signature ' . $search . ')'; + } elseif ($field == self::SEARCH_TITLE) { + $where = '(title ' . $search . ' OR mail_title ' . $search . ')'; + } else { + $where = $field . $search; + } + $list = XDB::fetchColumn('SELECT DISTINCT(id) + FROM newsletter_issues + WHERE nlid = {?} AND state = \'sent\' AND ' . $where . ' + ORDER BY date DESC', + $this->id); + + $issues = array(); + foreach ($list as $id) { + $issue = new NLIssue($id, $this, false); + if ($issue->checkUser($user)) { + $issues[] = $issue; + } + } + return $issues; + } + + public function articleSearch($search, $field, $user) + { + $search = XDB::formatWildcards(XDB::WILDCARD_CONTAINS, $search); + if ($field == self::SEARCH_ALL) { + $where = '(a.title ' . $search . ' OR a.body ' . $search . ' OR a.append ' . $search . ')'; + } else { + $where = 'a.' . $field . $search; + } + $list = XDB::fetchAllAssoc('SELECT i.short_name, a.aid, i.id, a.title + FROM newsletter_art AS a + INNER JOIN newsletter_issues AS i ON (a.id = i.id) + WHERE i.nlid = {?} AND i.state = \'sent\' AND ' . $where . ' + GROUP BY a.id, a.aid + ORDER BY i.date DESC, a.aid', + $this->id); + + $articles = array(); + foreach ($list as $item) { + $issue = new NLIssue($item['id'], $this, false); + if ($issue->checkUser($user)) { + $articles[] = $item; + } + } + return $articles; + } + // }}} // {{{ Subscription related function @@ -259,7 +320,7 @@ class NewsLetter * @p $hash True if the uid is actually a hash. * @return True if the user was successfully unsubscribed. */ - public function unsubscribe($uid = null, $hash = false) + public function unsubscribe($issue_id = null, $uid = null, $hash = false) { if (is_null($uid) && $hash) { // Unable to unsubscribe from an empty hash @@ -280,6 +341,12 @@ class NewsLetter XDB::execute('DELETE FROM newsletter_ins WHERE nlid = {?} AND uid = {?}', $this->id, $user); + if (!is_null($issue_id)) { + XDB::execute('UPDATE newsletter_issues + SET unsubscribe = unsubscribe + 1 + WHERE id = {?}', + $id); + } return true; } @@ -298,6 +365,21 @@ class NewsLetter } } + /** Subscribe a batch of users to a newsletter. + * This skips 'maySubscribe' test. + * + * @p $user_ids Array of user IDs to subscribe to the newsletter. + */ + public function bulkSubscribe($user_ids) + { + // TODO: use a 'bulkMaySubscribe'. + XDB::execute('INSERT IGNORE INTO newsletter_ins (nlid, uid, last, hash) + SELECT {?}, a.uid, NULL, NULL + FROM accounts AS a + WHERE a.uid IN {?}', + $this->id, $user_ids); + } + /** Retrieve subscription state of a user * @p $user Target user; if null, use current user. * @return Boolean: true if the user has subscribed to the NL. @@ -317,11 +399,32 @@ class NewsLetter /** Get the count of subscribers to the NL. * @return Number of subscribers. */ - public function subscriberCount() + public function subscriberCount($lost = null, $sex = null, $grade = null, $first_promo = null, $last_promo = null) { - return XDB::fetchOneCell('SELECT COUNT(uid) - FROM newsletter_ins - WHERE nlid = {?}', $this->id); + $cond = new PFC_And(new UFC_NLSubscribed($this->id)); + if (!is_null($sex)) { + $cond->addChild(new UFC_Sex($sex)); + } + if (!is_null($grade)) { + $cond->addChild(new UFC_Promo('>=', $grade, $first_promo)); + $cond->addChild(new UFC_Promo('<=', $grade, $last_promo)); + } + if (!($lost === null)) { + if ($lost === true) { + $cond->addChild(new PFC_Not(new UFC_HasEmailRedirect())); + } else { + $cond->addChild(new UFC_HasEmailRedirect()); + } + } + $uf = new UserFilter($cond); + return $uf->getTotalCount(); + } + + /** Get the count of subscribers with non valid redirection. + */ + public function lostSubscriberCount($sex = null) + { + return $this->subscriberCount(true, $sex); } /** Get the number of subscribers to the NL whose last received mailing was $last. @@ -433,10 +536,14 @@ class NewsLetter /** Get the prefix leading to the page for this NL * Only X.org / AX / X groups may be seen on X.org. */ - public function prefix() + public function prefix($enforce_xnet=true, $with_group=true) { if (!empty($GLOBALS['IS_XNET_SITE'])) { - return $this->group . '/nl'; + if ($with_group) { + return $this->group . '/nl'; + } else { + return 'nl'; + } } switch ($this->group) { case self::GROUP_XORG: @@ -445,18 +552,24 @@ class NewsLetter return 'ax'; case self::GROUP_EP: return 'epletter'; + case self::GROUP_FX: + return 'fxletter'; default: // Don't display groups NLs on X.org - assert(false); + assert(!$enforce_xnet); } } /** Get the prefix to use for all 'admin' pages of this NL. */ - public function adminPrefix() + public function adminPrefix($enforce_xnet=true, $with_group=true) { if (!empty($GLOBALS['IS_XNET_SITE'])) { - return $this->group . '/admin/nl'; + if ($with_group) { + return $this->group . '/admin/nl'; + } else { + return 'admin/nl'; + } } switch ($this->group) { case self::GROUP_XORG: @@ -465,10 +578,49 @@ class NewsLetter return 'ax/admin'; case self::GROUP_EP: return 'epletter/admin'; + case self::GROUP_FX: + return 'fxletter/admin'; default: // Don't display groups NLs on X.org - assert(false); + assert(!$enforce_xnet); + } + } + + /** Get the prefix to use for all 'stat' pages of this NL. + */ + public function statPrefix($enforce_xnet = true, $with_group = true) + { + if (!empty($GLOBALS['IS_XNET_SITE'])) { + if ($with_group) { + return $this->group . '/stat/nl'; + } else { + return 'stat/nl'; + } } + switch ($this->group) { + case self::GROUP_XORG: + return 'stat/newsletter'; + case self::GROUP_AX: + return 'ax/stat'; + case self::GROUP_EP: + return 'epletter/stat'; + case self::GROUP_FX: + return 'fxletter/stat'; + default: + // Don't display groups NLs on X.org + assert(!$enforce_xnet); + } + } + + /** Get links for nl pages. + */ + public function adminLinks() + { + return array( + 'index' => array('link' => $this->prefix(), 'title' => 'Archives'), + 'admin' => array('link' => $this->adminPrefix(), 'title' => 'Administrer'), + 'stats' => array('link' => $this->statPrefix(), 'title' => 'Statistiques') + ); } /** Hack used to remove "admin" links on X.org page on X.net @@ -491,6 +643,19 @@ class NewsLetter return $this->custom_css; } + public function canSyncWithGroup() + { + switch ($this->group) { + case self::GROUP_XORG: + case self::GROUP_AX: + case self::GROUP_EP: + case self::GROUP_FX: + return false; + default: + return true; + } + } + // }}} } @@ -520,6 +685,7 @@ class NLIssue public $send_before; // Date at which issue should be sent public $head; // Foreword of the issue (or body for letters with no articles) public $signature; // Signature of the letter + public $reply_to; // Adress to reply to the message (can be empty) public $arts = array(); // Articles of the issue const BATCH_SIZE = 60; // Number of emails to send every minute. @@ -544,7 +710,7 @@ class NLIssue { // Load this issue $res = XDB::query('SELECT nlid, short_name, date, send_before, state, sufb_json, - title, mail_title, head, signature + title, mail_title, head, signature, reply_to FROM newsletter_issues WHERE id = {?}', $id); @@ -558,6 +724,7 @@ class NLIssue $this->nl = new NewsLetter($issue['nlid']); } $this->id = $id; + $this->nlid = $issue['nlid']; $this->shortname = $issue['short_name']; $this->date = $issue['date']; $this->send_before = $issue['send_before']; @@ -566,6 +733,7 @@ class NLIssue $this->title_mail = $issue['mail_title']; $this->head = $issue['head']; $this->signature = $issue['signature']; + $this->reply_to = $issue['reply_to']; $this->sufb = $this->importJSonStoredUFB($issue['sufb_json']); if ($fetch_articles) { @@ -668,11 +836,8 @@ class NLIssue if ($success) { global $globals; $mailer = new PlMailer('newsletter/notify_scheduled.mail.tpl'); - $mailer->assign('group', $this->nl->group); - $mailer->assign('nl_title', $this->title_mail); - $mailer->assign('nl_id', $this->id()); + $mailer->assign('issue', $this); $mailer->assign('base', $globals->baseurl); - $mailer->assign('send_before', $this->send_before); $mailer->send(); $this->refresh(); } @@ -689,7 +854,7 @@ class NLIssue { if ($this->state == self::STATE_PENDING) { $success = XDB::execute('UPDATE newsletter_issues - SET send_before = NULL, state = \'new\' + SET state = \'new\' WHERE id = {?}', $this->id); if ($success) { $this->refresh(); @@ -759,7 +924,11 @@ class NLIssue public function last() { if (is_null($this->id_last)) { - $this->id_last = $this->nl->getIssue('last')->id; + try { + $this->id_last = $this->nl->getIssue('last')->id; + } catch (MailNotFound $e) { + $this->id_last = null; + } } return $this->id_last; } @@ -767,8 +936,10 @@ class NLIssue // }}} // {{{ Edition, articles + const ERROR_INVALID_REPLY_TO = 'invalid_reply_to'; const ERROR_INVALID_SHORTNAME = 'invalid_shortname'; const ERROR_INVALID_UFC = 'invalid_ufc'; + const ERROR_TOO_LONG_UFC = 'too_long_ufc'; const ERROR_SQL_SAVE = 'sql_error'; /** Save the global properties of this NL issue (title&co). @@ -785,6 +956,12 @@ class NLIssue 'signature' => $this->signature, ); + if (!empty($this->reply_to) && !isvalid_email($this->reply_to)) { + $errors[] = self::ERROR_INVALID_REPLY_TO ; + } else { + $fields['reply_to'] = $this->reply_to; + } + if ($this->isEditable()) { $fields['date'] = $this->date; if (!preg_match('/^[-a-z0-9]+$/i', $this->shortname) || is_numeric($this->shortname)) { @@ -794,6 +971,11 @@ class NLIssue } if ($this->sufb->isValid() || $this->sufb->isEmpty()) { $fields['sufb_json'] = json_encode($this->sufb->export()->dict()); + // If sufb_json is too long to be store, we do not store a truncated json and notify the user. + // The limit is LONGTEXT's one, ie 2^32 = 4294967296. + if (strlen($fields['sufb_json']) > 4294967295) { + $errors[] = self::ERROR_TOO_LONG_UFC; + } } else { $errors[] = self::ERROR_INVALID_UFC; } @@ -999,7 +1181,7 @@ class NLIssue */ public function isEmpty() { - return $this->mail_title == '' || $this->mail_title == 'to be continued' || (count($this->arts == 0 && strlen($this->head) == 0)); + return $this->title_mail == '' || $this->title_mail == 'to be continued' || (count($this->arts) == 0 && strlen($this->head) == 0); } /** Retrieve the 'Send before' date, in a clean format. @@ -1056,6 +1238,9 @@ class NLIssue $mailer->assign('user', $user); $mailer->assign('prefix', null); $mailer->assign('hash', $hash); + if (!empty($this->reply_to)) { + $mailer->addHeader('Reply-To', $this->reply_to); + } $mailer->sendTo($user); } @@ -1093,21 +1278,24 @@ class NLIssue WHERE id = {?}', $this->id); - $ufc = new PFC_And($this->getRecipientsUFC(), new UFC_NLSubscribed($this->nl->id, $this->id), new UFC_HasEmailRedirect()); - $emailsCount = 0; - $uf = new UserFilter($ufc, array(new UFO_IsAdmin(), new UFO_Uid())); + $ufc = new PFC_And($this->getRecipientsUFC(), new UFC_NLSubscribed($this->nl->id, $this->id), new UFC_HasValidEmail()); + $uf = new UserFilter($ufc, array(new UFO_IsAdmin(true), new UFO_Uid())); $limit = new PlLimit(self::BATCH_SIZE); + $global_sent = array(); while (true) { $sent = array(); $users = $uf->getUsers($limit); if (count($users) == 0) { - return $emailsCount; + break; } foreach ($users as $user) { + if (array_key_exists($user->id(), $global_sent)) { + Platal::page()->kill('Sending the same newsletter issue ' . $this->id . ' to user ' . $user->id() . ' twice, something must be wrong.'); + } $sent[] = $user->id(); + $global_sent[$user->id()] = true; $this->sendTo($user, $hash); - ++$emailsCount; } XDB::execute("UPDATE newsletter_ins SET last = {?} @@ -1115,7 +1303,7 @@ class NLIssue sleep(60); } - return $emailsCount; + return count($global_sent); } // }}} @@ -1127,7 +1315,8 @@ class NLIssue class NLArticle { // Maximum number of lines per article - const MAX_LINES_PER_ARTICLE = 9; + const MAX_LINES_PER_ARTICLE = 8; + const MAX_CHARACTERS_PER_LINE = 68; // {{{ properties @@ -1215,17 +1404,30 @@ class NLArticle public function check() { - $text = MiniWiki::WikiToText($this->body); - $arr = explode("\n",wordwrap($text,68)); - $c = 0; - foreach ($arr as $line) { - if (trim($line)) { - $c++; + $rest = $this->remain(); + + return $rest['remaining_lines'] >= 0; + } + + // }}} + // {{{ function remain() + + public function remain() + { + $text = MiniWiki::WikiToText($this->body); + $array = explode("\n", wordwrap($text, self::MAX_CHARACTERS_PER_LINE)); + $lines_count = 0; + foreach ($array as $line) { + if (trim($line) != '') { + ++$lines_count; } } - return $c < self::MAX_LINES_PER_ARTICLE; - } + return array( + 'remaining_lines' => self::MAX_LINES_PER_ARTICLE - $lines_count, + 'remaining_characters_for_last_line' => self::MAX_CHARACTERS_PER_LINE - strlen($array[count($array) - 1]) + ); + } // }}} // {{{ function parseUrlsFromArticle()