From 84163d5890a4217f6087b274051dcc99cbbafc29 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 24 Jan 2011 01:23:54 +0100 Subject: [PATCH] Update/merge Newsletter-related code. (Closes #1226, #858, #1047) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- .../{axletter.send.php => newsletters.send.php} | 12 +- classes/xnet.php | 2 +- configs/platal.cron.in | 4 +- htdocs/css/{ax.css => nl.AX.css} | 0 htdocs/css/{nl.css => nl.Polytechnique.org.css} | 0 include/massmailer.inc.php | 253 ----- include/newsletter.inc.php | 1148 ++++++++++++++++++-- include/validations/nl.inc.php | 8 +- modules/admin.php | 2 +- modules/axletter.php | 364 +------ modules/axletter/axletter.inc.php | 245 ----- modules/newsletter.php | 256 ++++- modules/xnetnl.php | 48 + templates/admin/index.tpl | 6 +- templates/axletter/admin.tpl | 51 - templates/axletter/edit.tpl | 121 --- templates/axletter/index.tpl | 83 -- templates/axletter/show.tpl | 59 - templates/axletter/unsubscribe.tpl | 41 - templates/include/form.valid.edit-nl.tpl | 6 +- templates/include/massmailer-nav.tpl | 14 +- templates/newsletter/admin.tpl | 16 +- templates/newsletter/edit.tpl | 154 ++- templates/newsletter/index.tpl | 30 +- .../letter.mail.tpl => newsletter/nl.AX.mail.tpl} | 19 +- .../{nl.mail.tpl => nl.Polytechnique.org.mail.tpl} | 30 +- templates/newsletter/show.tpl | 16 +- upgrade/1.1.0/20_newsletter.sql | 171 +++ 28 files changed, 1703 insertions(+), 1456 deletions(-) rename bin/cron/{axletter.send.php => newsletters.send.php} (89%) rename htdocs/css/{ax.css => nl.AX.css} (100%) rename htdocs/css/{nl.css => nl.Polytechnique.org.css} (100%) delete mode 100644 include/massmailer.inc.php delete mode 100644 modules/axletter/axletter.inc.php create mode 100644 modules/xnetnl.php delete mode 100644 templates/axletter/admin.tpl delete mode 100644 templates/axletter/edit.tpl delete mode 100644 templates/axletter/index.tpl delete mode 100644 templates/axletter/show.tpl delete mode 100644 templates/axletter/unsubscribe.tpl rename templates/{axletter/letter.mail.tpl => newsletter/nl.AX.mail.tpl} (89%) rename templates/newsletter/{nl.mail.tpl => nl.Polytechnique.org.mail.tpl} (85%) create mode 100644 upgrade/1.1.0/20_newsletter.sql diff --git a/bin/cron/axletter.send.php b/bin/cron/newsletters.send.php similarity index 89% rename from bin/cron/axletter.send.php rename to bin/cron/newsletters.send.php index 5fa8827..e9e4ab5 100755 --- a/bin/cron/axletter.send.php +++ b/bin/cron/newsletters.send.php @@ -21,16 +21,16 @@ ***************************************************************************/ require_once './connect.db.inc.php'; -require_once '../../modules/axletter/axletter.inc.php'; +require_once 'newsletter.inc.php'; ini_set('memory_limit', '128M'); -$al = AXLetter::toSend(); -if ($al) { - echo "Envoi de la lettre \"{$al->title()}\"\n\n"; +$nls = NewsLetter::getIssuesToSend(); +foreach ($nls as $nl) { + echo "Envoi de la lettre \"{$nl->title()}\" (Groupe {$nl->group})\n\n"; echo ' ' . date("H:i:s") . " -> début de l'envoi\n"; - $emailsCount = $al->sendToAll(); + $emailsCount = $nl->sendToAll(); echo ' ' . date("H:i:s") . " -> fin de l'envoi\n\n"; - echo $emailsCount . " emails ont été envoyés lors de cet envoi.\n"; + echo $emailsCount . " emails ont été envoyés lors de cet envoi.\n\n"; } // vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: diff --git a/classes/xnet.php b/classes/xnet.php index 5b42fa1..fe5f421 100644 --- a/classes/xnet.php +++ b/classes/xnet.php @@ -24,7 +24,7 @@ class Xnet extends Platal public function __construct() { parent::__construct('xnet', 'xnetgrp', 'xnetlists', 'xnetevents', - 'payment', 'bandeau'); + 'payment', 'bandeau', 'xnetnl'); } public function hook_map($name) diff --git a/configs/platal.cron.in b/configs/platal.cron.in index bb88941..f1e5936 100644 --- a/configs/platal.cron.in +++ b/configs/platal.cron.in @@ -26,8 +26,8 @@ WD=/home/web/prod/platal/bin/cron # flux rss de banana */5 * * * * www-data cd $WD; ./banana.feedgen.php > /dev/null -# AX spammer -15 * * * * web cd $WD; ./axletter.send.php | mail -e -s "Envoi d'un email de l'AX" br@staff.m4x.org +# Send group Newsletters +15 * * * * web cd $WD; ./newsletters.send.php | mail -e -s "Envoi des NLs des groupes" br@staff.m4x.org # homonymes 0 0 4 * * web cd $WD; ./homonyms.php diff --git a/htdocs/css/ax.css b/htdocs/css/nl.AX.css similarity index 100% rename from htdocs/css/ax.css rename to htdocs/css/nl.AX.css diff --git a/htdocs/css/nl.css b/htdocs/css/nl.Polytechnique.org.css similarity index 100% rename from htdocs/css/nl.css rename to htdocs/css/nl.Polytechnique.org.css diff --git a/include/massmailer.inc.php b/include/massmailer.inc.php deleted file mode 100644 index b38c961..0000000 --- a/include/massmailer.inc.php +++ /dev/null @@ -1,253 +0,0 @@ -_tpl = $tpl; - $this->_css = $css; - $this->_prefix = $prefix; - $this->_table = $tbl; - $this->_subscriptionTable = $stbl; - } - - public function id() - { - return is_null($this->_shortname) ? $this->_id : $this->_shortname; - } - - private function selectId($where) - { - $res = XDB::query("SELECT IF (n.short_name IS NULL, n.id, n.short_name) - FROM {$this->_table} AS n - WHERE n.bits != 'new' AND {$where} - LIMIT 1"); - if ($res->numRows() != 1) { - return null; - } - return $res->fetchOneCell(); - } - - public function prev() - { - static $val; - if (!isset($val)) { - $val = $this->selectId("n.id < {$this->_id} ORDER BY n.id DESC"); - } - return $val; - } - - public function next() - { - static $val; - if (!isset($val)) { - $val = $this->selectId("n.id > {$this->_id} ORDER BY n.id"); - } - return $val; - } - - public function last() - { - static $val; - if (!isset($val)) { - $res = XDB::query("SELECT MAX(n.id) - FROM {$this->_table} AS n - WHERE n.bits != 'new' AND n.id > {?}", - $this->_id); - if ($res->numRows() != 1) { - $val = null; - } else { - $val = $res->fetchOneCell(); - } - } - return $val; - } - - public function title($mail = false) - { - return $mail ? $this->_title_mail : $this->_title; - } - - public function head($user = null, $type = 'text') - { - if (is_null($user)) { - return $this->_head; - } else { - $head = $this->_head; - $head = str_replace('', $user->isFemale() ? 'Chère' : 'Cher', $head); - $head = str_replace('', $user->displayName(), $head); - $head = str_replace('', '', $head); - return format_text($head, $type, 2, 64); - } - } - - public function css(&$page = null) - { - if (!is_null($page)) { - $page->addCssLink($this->_css); - return true; - } else { - $css = file_get_contents(dirname(__FILE__) . '/../htdocs/css/' . $this->_css); - return preg_replace('@/\*.*?\*/@us', '', $css); - } - } - - public function toText(&$page, $user) - { - $this->css($page); - $page->assign('is_mail', false); - $page->assign('mail_part', 'text'); - $page->assign('user', $user); - $this->assignData($page); - } - - public function toHtml(&$page, $user) - { - $this->css($page); - $page->assign('prefix', $this->_prefix . '/' . $this->id()); - $page->assign('is_mail', false); - $page->assign('mail_part', 'html'); - $page->assign('user', $user); - $this->assignData($page); - } - - private function createHash($line, $key = null) - { - $hash = implode(time(), $line) . rand(); - $hash = md5($hash); - return $hash; - } - - public function sendTo($user, $hash = null) - { - if (is_null($hash)) { - $hash = XDB::fetchOneCell("SELECT hash - FROM {$this->_subscriptionTable} - WHERE uid = {?}", $user->id()); - } - if (is_null($hash)) { - $hash = $this->createHash(array($user->displayName(), $user->fullName(), - $user->isFemale(), $user->isEmailFormatHtml(), - rand(), "X.org rulez")); - XDB::execute("UPDATE {$this->_subscriptionTable} as ni - SET ni.hash = {?} - WHERE ni.uid = {?}", - $hash, $user->id()); - } - - $mailer = new PlMailer($this->_tpl); - $this->assignData($mailer); - $mailer->assign('is_mail', true); - $mailer->assign('user', $user); - $mailer->assign('prefix', null); - $mailer->assign('hash', $hash); - $mailer->addTo('"' . $user->fullName() . '" <' . $user->bestEmail() . '>'); - $mailer->send($user->isEmailFormatHtml()); - } - - protected function getAllRecipients() - { - global $globals; - return "SELECT a.uid - FROM {$this->_subscriptionTable} AS ni - INNER JOIN accounts AS a ON (ni.uid = a.uid) - LEFT JOIN email_options AS eo ON (eo.uid = a.uid) - LEFT JOIN emails AS e ON (e.uid = a.uid AND e.flags='active') - LEFT JOIN account_profiles AS ap ON (a.uid = ap.uid AND FIND_IN_SET('owner', ap.perms)) - LEFT JOIN profile_display AS pd ON (ap.pid = pd.pid) - WHERE ni.last < {?} AND ({$this->subscriptionWhere()}) AND - (e.email IS NOT NULL OR FIND_IN_SET('googleapps', eo.storage)) - GROUP BY a.uid"; - } - - public function sendToAll() - { - $this->setSent(); - $query = XDB::format($this->getAllRecipients(), $this->_id) . ' LIMIT 60'; - $emailsCount = 0; - - while (true) { - $sent = array(); - $users = User::getBulkUsersWithUIDs(XDB::fetchColumn($query)); - if (count($users) == 0) { - return $emailsCount; - } - foreach ($users as $user) { - $sent[] = $user->id(); - $this->sendTo($user, $hash); - ++$emailsCount; - } - XDB::execute("UPDATE {$this->_subscriptionTable} - SET last = {?} - WHERE uid IN {?}", $this->_id, $sent); - - sleep(60); - } - return $emailsCount; - } - - abstract protected function assignData(&$smarty); - abstract protected function setSent(); - - abstract protected function subscriptionWhere(); -} - -// }}} -// {{{ Functions - -function format_text($input, $format, $indent = 0, $width = 68) -{ - if ($format == 'text') { - return MiniWiki::WikiToText($input, true, $indent, $width, "title"); - } - return MiniWiki::WikiToHTML($input, "title"); -} - -// function enriched_to_text($input,$html=false,$just=false,$indent=0,$width=68) - -// }}} - -// vim:set et sw=4 sts=4 sws=4 enc=utf-8: -?> diff --git a/include/newsletter.inc.php b/include/newsletter.inc.php index 4e3aa7f..6a86929 100644 --- a/include/newsletter.inc.php +++ b/include/newsletter.inc.php @@ -19,170 +19,1085 @@ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ***************************************************************************/ -require_once("massmailer.inc.php"); +// {{{ class MailNotFound + +class MailNotFound extends Exception { +} + +// }}} // {{{ class NewsLetter -class NewsLetter extends MassMailer +class NewsLetter { - public $_date; - public $_cats = array(); - public $_arts = array(); + public $id; // ID of the NL (in table newsletters) + public $group; // Short name of the group corresponding to the NL + public $group_id; // ID of that group + public $name; // Name of the NL (e.g "Lettre de Polytechnique.org", ...) + public $cats; // List of all categories for this NL + public $criteria; // PlFlagSet of allowed filters for recipient selection + + protected $custom_css = false; - function __construct($id = null) + // Base name to use instead of the group short name for NLs without a custom CSS + const FORMAT_DEFAULT_GROUP = 'default'; + + // Diminutif of X.net groups with a specific NL view + const GROUP_XORG = 'Polytechnique.org'; + const GROUP_AX = 'AX'; + const GROUP_EP = 'Ecole'; + + // {{{ Constructor, NewsLetter retrieval (forGroup, getAll) + + public function __construct($id) { - parent::__construct('newsletter/nl.mail.tpl', 'nl.css', 'nl/show', 'newsletter', 'newsletter_ins'); - if (isset($id)) { - if ($id == 'last') { - $res = XDB::query("SELECT MAX(id) FROM newsletter WHERE bits!='new'"); - $id = $res->fetchOneCell(); + // Load NL data + $res = XDB::query('SELECT nls.group_id, g.diminutif AS group_name, + nls.name AS nl_name, nls.custom_css, nls.criteria + FROM newsletters AS nls + LEFT JOIN groups AS g ON (nls.group_id = g.id) + WHERE nls.id = {?}', + $id); + if (!$res->numRows()) { + throw new MailNotFound(); + } + + $data = $res->fetchOneAssoc(); + $this->id = $id; + $this->group_id = $data['group_id']; + $this->group = $data['group_name']; + $this->name = $data['nl_name']; + $this->custom_css = $data['custom_css']; + $this->criteria = new PlFlagSet($data['criteria']); + + // Load the categories + $res = XDB::iterRow( + 'SELECT cid, title + FROM newsletter_cat + WHERE nlid = {?} + ORDER BY pos', $id); + while (list($cid, $title) = $res->next()) { + $this->cats[$cid] = $title; + } + } + + /** Retrieve the NL associated with a given group. + * @p $group Short name of the group + * @return A NewsLetter object, or null if the group doesn't have a NL. + */ + public static function forGroup($group) + { + $res = XDB::query('SELECT nls.id + FROM newsletters AS nls + LEFT JOIN groups AS g ON (nls.group_id = g.id) + WHERE g.diminutif = {?}', $group); + if (!$res->numRows()) { + return null; + } + return new NewsLetter($res->fetchOneCell()); + } + + /** Retrieve all newsletters + * @return An array of $id => NewsLetter objects + */ + public static function getAll() + { + $res = XDB::query('SELECT id + FROM newsletters'); + $nls = array(); + foreach ($res->fetchColumn() as $id) { + $nls[$id] = new NewsLetter($id); + } + return $nls; + } + + // }}} + // {{{ Issue retrieval + + /** Retrieve all issues which should be sent + * @return An array of NLIssue objects to send (i.e state = 'new' and send_before <= today) + */ + public static function getIssuesToSend() + { + $res = XDB::query('SELECT id + FROM newsletter_issues + WHERE state = \'pending\' AND send_before <= NOW()'); + $issues = array(); + foreach ($res->fetchColumn() as $id) { + $issues[$id] = new NLIssue($id); + } + return $issues; + } + + /** Retrieve a given issue of this NewsLetter + * @p $name Name or ID of the issue to retrieve. + * @return A NLIssue object. + * + * $name may be either a short_name, an ID or the special value 'last' which + * selects the latest sent NL. + * If $name is null, this will retrieve the current pending NL. + */ + public function getIssue($name = null, $only_sent = true) + { + if ($name) { + if ($name == 'last') { + if ($only_sent) { + $where = 'state = \'sent\' AND '; + } else { + $where = ''; + } + $res = XDB::query('SELECT MAX(id) + FROM newsletter_issues + WHERE ' . $where . ' nlid = {?}', + $this->id); + } else { + $res = XDB::query('SELECT id + FROM newsletter_issues + WHERE nlid = {?} AND (id = {?} OR short_name = {?})', + $this->id, $name, $name); } - $res = XDB::query("SELECT * FROM newsletter WHERE id={?} OR short_name={?} LIMIT 1", $id, $id); - } else { - $res = XDB::query("SELECT * FROM newsletter WHERE bits='new'"); if (!$res->numRows()) { - NewsLetter::create(); + throw new MailNotFound(); + } + $id = $res->fetchOneCell(); + } else { + $query = XDB::format('SELECT id + FROM newsletter_issues + WHERE nlid = {?} AND state = \'new\' + ORDER BY id DESC', $this->id); + $res = XDB::query($query); + if ($res->numRows()) { + $id = $res->fetchOneCell(); + } else { + // Create a new, empty issue, and return it + $id = $this->createPending(); } - $res = XDB::query("SELECT * FROM newsletter WHERE bits='new' ORDER BY id DESC LIMIT 1"); } - if ($res->numRows() != 1) { + + return new NLIssue($id, &$this); + } + + /** Create a new, empty, pending newsletter issue + * @p $nlid The id of the NL for which a new pending issue should be created. + * @return Id of the newly created issue. + */ + public function createPending() + { + XDB::execute('INSERT INTO newsletter_issues + SET nlid = {?}, state=\'new\', date=NOW(), + title=\'to be continued\', + mail_title=\'to be continued\'', + $this->id); + return XDB::insertId(); + } + + /** Return all sent issues of this newsletter. + * @return An array of (id => NLIssue) + */ + public function listSentIssues($check_user = false, $user = null) + { + if ($check_user && $user == null) { + $user = S::user(); + } + + $res = XDB::query('SELECT id + FROM newsletter_issues + WHERE nlid = {?} AND state = \'sent\' + ORDER BY date DESC', $this->id); + $issues = array(); + foreach ($res->fetchColumn() as $id) { + $issue = new NLIssue($id, $this, false); + if (!$check_user || $issue->checkUser($user)) { + $issues[$id] = $issue; + } + } + return $issues; + } + + /** Return all issues of this newsletter, including invalid and sent. + * @return An array of (id => NLIssue) + */ + public function listAllIssues() + { + $res = XDB::query('SELECT id + FROM newsletter_issues + WHERE nlid = {?} + ORDER BY FIELD(state, \'pending\', \'new\') DESC, date DESC', $this->id); + $issues = array(); + foreach ($res->fetchColumn() as $id) { + $issues[$id] = new NLIssue($id, $this, false); + } + return $issues; + } + + /** Return the latest pending issue of the newsletter. + * @return Either null, or a NL object. + */ + public function getPendingIssue() + { + $res = XDB::query('SELECT MAX(id) + FROM newsletter_issues + WHERE nlid = {?} AND state = \'new\'', + $this->id); + if ($res->numRows()) { + $id = $res->fetchOneCell(); + return new NLIssue($id, $this); + } else { + return null; + } + } + + // }}} + // {{{ Subscription related function + + /** Unsubscribe a user from this newsletter + * @p $uid UID to unsubscribe from the newsletter; if null, use current user. + * @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) + { + if (is_null($uid) && $hash) { + // Unable to unsubscribe from an empty hash + return false; + } + $user = is_null($uid) ? S::user()->id() : $uid; + $field = $hash ? 'hash' : 'uid'; + $res = XDB::query('SELECT uid + FROM newsletter_ins + WHERE nlid = {?} AND ' . $field . ' = {?}', + $this->id, $user); + if (!$res->numRows()) { + // No subscribed user with that UID/hash + return false; + } + $user = $res->fetchOneCell(); + + XDB::execute('DELETE FROM newsletter_ins + WHERE nlid = {?} AND uid = {?}', + $this->id, $user); + return true; + } + + /** Subscribe a user to a newsletter + * @p $user User to subscribe to the newsletter; if null, use current user. + */ + public function subscribe($user = null) + { + if (is_null($user)) { + $user = S::user(); + } + if (self::maySubscribe($user)) { + XDB::execute('INSERT IGNORE INTO newsletter_ins (nlid, uid, last, hash) + VALUES ({?}, {?}, NULL, hash)', + $this->id, $user->id()); + } + } + + /** 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. + */ + public function subscriptionState($user = null) + { + if (is_null($user)) { + $user = S::user(); + } + $res = XDB::query('SELECT 1 + FROM newsletter_ins + WHERE nlid = {?} AND uid = {?}', + $this->id, $user->id()); + return ($res->numRows() == 1); + } + + /** Get the count of subscribers to the NL. + * @return Number of subscribers. + */ + public function subscriberCount() + { + return XDB::fetchOneCell('SELECT COUNT(uid) + FROM newsletter_ins + WHERE nlid = {?}', $this->id); + } + + /** Get the number of subscribers to the NL whose last received mailing was $last. + * @p $last ID of the issue for which subscribers should be counted. + * @return Number of subscribers + */ + public function subscriberCountForLast($last) + { + return XDB::fetchOneCell('SELECT COUNT(uid) + FROM newsletter_ins + WHERE nlid = {?} AND last = {?}', $this->id, $last); + } + + /** Retrieve the list of newsletters a user has subscribed to + * @p $user User whose subscriptions should be retrieved (if null, use session user). + * @return Array of newsletter IDs + */ + public static function getUserSubscriptions($user = null) + { + if (is_null($user)) { + $user = S::user(); + } + $res = XDB::query('SELECT nlid + FROM newsletter_ins + WHERE uid = {?}', + $user->id()); + return $res->fetchColumn(); + } + + /** Retrieve the UserFilterBuilder for subscribers to this NL. + * This is the place where NL-specific filters may be allowed or prevented. + * @p $envprefix Prefix to use for env fields (cf. UserFilterBuilder) + * @return A UserFilterBuilder object using the given env prefix + */ + public function getSubscribersUFB($envprefix = '') + { + require_once 'ufbuilder.inc.php'; + return new UFB_NewsLetter($this->criteria, $envprefix); + } + + // }}} + // {{{ Permissions related functions + + /** For later use: check whether a given user may subscribe to this newsletter. + * @p $user User whose access should be checked + * @return Boolean: whether the user may subscribe to the NL. + */ + public function maySubscribe($user = null) + { + return true; + } + + /** Whether a given user may edit this newsletter + * @p $uid UID of the user whose perms should be checked (if null, use current user) + * @return Boolean: whether the user may edit the NL + */ + public function mayEdit($user = null) + { + if (is_null($user)) { + $user = S::user(); + } + if ($user->checkPerms('admin')) { + return true; + } + $res = XDB::query('SELECT perms + FROM group_members + WHERE asso_id = {?} AND uid = {?}', + $this->group_id, $user->id()); + return ($res->numRows() && $res->fetchOneCell() == 'admin'); + } + + /** Whether a given user may submit articles to this newsletter using X.org validation system + * @p $user User whose access should be checked (if null, use current user) + * @return Boolean: whether the user may submit articles + */ + public function maySubmit($user = null) + { + // Submission of new articles is only enabled for the X.org NL (and forbidden when viewing issues on X.net) + return ($this->group == self::GROUP_XORG && !isset($GLOBALS['IS_XNET_SITE'])); + } + + // }}} + // {{{ Display-related functions: cssFile, tplFile, prefix, admin_prefix, admin_links_enabled, automatic_mailings_enabled + + /** Get the name of the css file used to display this newsletter. + */ + public function cssFile() + { + if ($this->custom_css) { + $base = $this->group; + } else { + $base = self::FORMAT_DEFAULT_GROUP; + } + return 'nl.' . $base . '.css'; + } + + /** Get the name of the template file used to display this newsletter. + */ + public function tplFile() + { + if ($this->custom_css) { + $base = $this->group; + } else { + $base = self::FORMAT_DEFAULT_GROUP; + } + return 'newsletter/nl.' . $base . '.mail.tpl'; + } + + /** 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() + { + if (!empty($GLOBALS['IS_XNET_SITE'])) { + return $this->group . '/nl'; + } + switch ($this->group) { + case self::GROUP_XORG: + return 'nl'; + case self::GROUP_AX: + return 'ax'; + case self::GROUP_EP: + return 'epletter'; + default: + // Don't display groups NLs on X.org + assert(false); + } + } + + /** Get the prefix to use for all 'admin' pages of this NL. + */ + public function adminPrefix() + { + if (!empty($GLOBALS['IS_XNET_SITE'])) { + return $this->group . '/admin/nl'; + } + switch ($this->group) { + case self::GROUP_XORG: + return 'admin/newsletter'; + case self::GROUP_AX: + return 'ax/admin'; + case self::GROUP_EP: + return 'epletter/admin'; + default: + // Don't display groups NLs on X.org + assert(false); + } + } + + /** Hack used to remove "admin" links on X.org page on X.net + * The 'admin' links are enabled for all pages, except for X.org when accessing NL through X.net + */ + public function adminLinksEnabled() + { + return ($this->group != self::GROUP_XORG || !isset($GLOBALS['IS_XNET_SITE'])); + } + + /** Automatic mailings are disabled for X.org NL. + */ + public function automaticMailingEnabled() + { + return $this->group != self::GROUP_XORG; + } + + public function hasCustomCss() + { + return $this->custom_css; + } + + // }}} +} + +// }}} + +// {{{ class NLIssue + +// A NLIssue is an issue of a given NewsLetter +class NLIssue +{ + protected $nlid; // Id of the newsletter + + const STATE_NEW = 'new'; // New, currently being edited + const STATE_PENDING = 'pending'; // Ready for mailing + const STATE_SENT = 'sent'; // Sent + + public $nl; // Related NL + + public $id; // Id of this issue of the newsletter + public $shortname; // Shortname for this issue + public $title; // Title of this issue + public $title_mail; // Title of the email + public $state; // State of the issue (one of the STATE_ values) + public $sufb; // Environment to use to generate the UFC through an UserFilterBuilder + + public $date; // Date at which this issue was sent + 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 $arts = array(); // Articles of the issue + + const BATCH_SIZE = 60; // Number of emails to send every minute. + + // {{{ Constructor, id-related functions + + /** Build a NewsLetter. + * @p $id: ID of the issue (unique among all newsletters) + * @p $nl: Optional argument containing an already built NewsLetter object. + */ + function __construct($id, $nl = null, $fetch_articles = true) + { + return $this->fetch($id, $nl, $fetch_articles); + } + + protected function refresh() + { + return $this->fetch($this->id, $this->nl, false); + } + + protected function fetch($id, $nl = null, $fetch_articles = true) + { + // Load this issue + $res = XDB::query('SELECT nlid, short_name, date, send_before, state, sufb_json, + title, mail_title, head, signature + FROM newsletter_issues + WHERE id = {?}', + $id); + if (!$res->numRows()) { throw new MailNotFound(); } - $nl = $res->fetchOneAssoc(); + $issue = $res->fetchOneAssoc(); + if ($nl && $nl->id == $issue['nlid']) { + $this->nl = $nl; + } else { + $this->nl = new NewsLetter($issue['nlid']); + } + $this->id = $id; + $this->shortname = $issue['short_name']; + $this->date = $issue['date']; + $this->send_before = $issue['send_before']; + $this->state = $issue['state']; + $this->title = $issue['title']; + $this->title_mail = $issue['mail_title']; + $this->head = $issue['head']; + $this->signature = $issue['signature']; + $this->sufb = $this->importJSonStoredUFB($issue['sufb_json']); - $this->_id = $nl['id']; - $this->_shortname = $nl['short_name']; - $this->_date = $nl['date']; - $this->_title = $nl['titre']; - $this->_title_mail = $nl['titre_mail']; - $this->_head = $nl['head']; + if ($fetch_articles) { + $this->fetchArticles(); + } + } - $res = XDB::iterRow("SELECT cid,titre FROM newsletter_cat ORDER BY pos"); - while (list($cid, $title) = $res->next()) { - $this->_cats[$cid] = $title; + protected function fetchArticles($force = false) + { + if (count($this->arts) && !$force) { + return; } + // Load the articles $res = XDB::iterRow( - "SELECT a.title,a.body,a.append,a.aid,a.cid,a.pos - FROM newsletter_art AS a - INNER JOIN newsletter AS n USING(id) - LEFT JOIN newsletter_cat AS c ON(a.cid=c.cid) - WHERE a.id={?} - ORDER BY c.pos,a.pos", $this->_id); + 'SELECT a.title, a.body, a.append, a.aid, a.cid, a.pos + FROM newsletter_art AS a + INNER JOIN newsletter_issues AS ni USING(id) + LEFT JOIN newsletter_cat AS c ON (a.cid = c.cid) + WHERE a.id = {?} + ORDER BY c.pos, a.pos', + $this->id); while (list($title, $body, $append, $aid, $cid, $pos) = $res->next()) { - $this->_arts[$cid]["a$aid"] = new NLArticle($title, $body, $append, $aid, $cid, $pos); + $this->arts[$cid][$aid] = new NLArticle($title, $body, $append, $aid, $cid, $pos); + } + } + + protected function importJSonStoredUFB($json = null) + { + require_once 'ufbuilder.inc.php'; + $ufb = $this->nl->getSubscribersUFB(); + if (is_null($json)) { + return new StoredUserFilterBuilder($ufb, new PFC_True()); + } + $export = json_decode($json, true); + if (is_null($export)) { + PlErrorReport::report("Invalid json while reading NL {$this->nlid}, issue {$this->id}: failed to import '''{$json}'''."); + return new StoredUserFilterBuilder($ufb, new PFC_True()); + } + $sufb = new StoredUserFilterBuilder($ufb); + $sufb->fillFromExport($export); + return $sufb; + } + + protected function exportStoredUFBAsJSon() + { + return json_encode($this->sufb->export()); + } + + public function id() + { + return is_null($this->shortname) ? $this->id : $this->shortname; + } + + protected function selectId($where) + { + $res = XDB::query("SELECT IFNULL(ni.short_name, ni.id) + FROM newsletter_issues AS ni + WHERE ni.state != 'new' AND ni.nlid = {?} AND ${where} + LIMIT 1", $this->nl->id); + if ($res->numRows() != 1) { + return null; + } + return $res->fetchOneCell(); + } + + /** Delete this issue + * @return True if the issue could be deleted, false otherwise. + * Related articles will be deleted through cascading FKs. + * If this issue was the last issue for at least one subscriber, the deletion will be aborted. + */ + public function delete() + { + if ($this->state == self::STATE_NEW) { + $res = XDB::query('SELECT COUNT(*) + FROM newsletter_ins + WHERE last = {?}', $this->id); + if ($res->fetchOneCell() > 0) { + return false; + } + + return XDB::execute('DELETE FROM newsletter_issues + WHERE id = {?}', $this->id); + } else { + return false; + } + } + + /** Schedule a mailing of this NL + * If the 'send_before' field was NULL, it is set to the current time. + * @return Boolean Whether the date could be set (false if trying to schedule an already sent NL) + */ + public function scheduleMailing() + { + if ($this->state == self::STATE_NEW) { + $success = XDB::execute('UPDATE newsletter_issues + SET state = \'pending\', send_before = IFNULL(send_before, NOW()) + WHERE id = {?}', + $this->id); + if ($success) { + $this->refresh(); + } + return $success; + } else { + return false; + } + } + + /** Cancel the scheduled mailing of this NL + * @return Boolean: whether the mailing could be cancelled. + */ + public function cancelMailing() + { + if ($this->state == self::STATE_PENDING) { + $success = XDB::execute('UPDATE newsletter_issues + SET send_before = NULL, state = \'new\' + WHERE id = {?}', $this->id); + if ($success) { + $this->refresh(); + } + return $success; + } else { + return false; + } + } + + /** Helper function for smarty templates: is this issue editable ? + */ + public function isEditable() + { + return $this->state == self::STATE_NEW; + } + + /** Helper function for smarty templates: is the mailing of this issue scheduled ? + */ + public function isPending() + { + return $this->state == self::STATE_PENDING; + } + + /** Helper function for smarty templates: has this issue been sent ? + */ + public function isSent() + { + return $this->state == self::STATE_SENT; + } + + // }}} + // {{{ Navigation + + private $id_prev = null; + private $id_next = null; + private $id_last = null; + + /** Retrieve ID of the previous issue + * That value, once fetched, is cached in the private $id_prev variable. + * @return ID of the previous issue. + */ + public function prev() + { + if (is_null($this->id_prev)) { + $this->id_prev = $this->selectId(XDB::format("ni.id < {?} ORDER BY ni.id DESC", $this->id)); + } + return $this->id_prev; + } + + /** Retrieve ID of the following issue + * That value, once fetched, is cached in the private $id_next variable. + * @return ID of the following issue. + */ + public function next() + { + if (is_null($this->id_next)) { + $this->id_next = $this->selectId(XDB::format("ni.id > {?} ORDER BY ni.id", $this->id)); + } + return $this->id_next; + } + + /** Retrieve ID of the last issue + * That value, once fetched, is cached in the private $id_last variable. + * @return ID of the last issue. + */ + public function last() + { + if (is_null($this->id_last)) { + $this->id_last = $this->nl->getIssue('last')->id; } + return $this->id_last; } + // }}} + // {{{ Edition, articles + + const ERROR_INVALID_SHORTNAME = 'invalid_shortname'; + const ERROR_INVALID_UFC = 'invalid_ufc'; + const ERROR_SQL_SAVE = 'sql_error'; + + /** Save the global properties of this NL issue (title&co). + */ public function save() { - XDB::execute('UPDATE newsletter SET date={?},titre={?},titre_mail={?},head={?},short_name={?} WHERE id={?}', - $this->_date, $this->_title, $this->_title_mail, $this->_head, $this->_shortname,$this->_id); + $errors = array(); + + // Fill the list of fields to update + $fields = array( + 'title' => $this->title, + 'mail_title' => $this->title_mail, + 'head' => $this->head, + 'signature' => $this->signature, + ); + + if ($this->isEditable()) { + $fields['date'] = $this->date; + if (!preg_match('/^[-a-z0-9]+$/i', $this->shortname) || is_numeric($this->shortname)) { + $errors[] = self::ERROR_INVALID_SHORTNAME; + } else { + $fields['short_name'] = $this->shortname; + } + if ($this->sufb->isValid() || $this->sufb->isEmpty()) { + $fields['sufb_json'] = json_encode($this->sufb->export()->dict()); + } else { + $errors[] = self::ERROR_INVALID_UFC; + } + + if ($this->nl->automaticMailingEnabled()) { + $fields['send_before'] = ($this->send_before ? $this->send_before : null); + } + } + + if (count($errors)) { + return $errors; + } + $field_sets = array(); + foreach ($fields as $key => $value) { + $field_sets[] = XDB::format($key . ' = {?}', $value); + } + XDB::execute('UPDATE newsletter_issues + SET ' . implode(', ', $field_sets) . ' + WHERE id={?}', + $this->id); + if (XDB::affectedRows()) { + $this->refresh(); + } else { + $errors[] = self::ERROR_SQL_SAVE; + } + return $errors; } + /** Get an article by number + * @p $aid Article ID (among articles of the issue) + * @return A NLArticle object, or null if there is no article by that number + */ public function getArt($aid) { - foreach ($this->_arts as $key=>$artlist) { - if (isset($artlist["a$aid"])) { - return $artlist["a$aid"]; + $this->fetchArticles(); + + foreach ($this->arts as $category => $artlist) { + if (isset($artlist[$aid])) { + return $artlist[$aid]; } } return null; } + /** Save an article + * @p &$a A reference to a NLArticle object (will be modified once saved) + */ public function saveArticle(&$a) { - $a->_cid = ($a->_cid == 0) ? null : $a->_cid; - if ($a->_aid >= 0) { + $this->fetchArticles(); + + // Prevent cid to be 0 (use NULL instead) + $a->cid = ($a->cid == 0) ? null : $a->cid; + if ($a->aid >= 0) { + // Article already exists in DB XDB::execute('UPDATE newsletter_art SET cid = {?}, pos = {?}, title = {?}, body = {?}, append = {?} WHERE id = {?} AND aid = {?}', - $a->_cid, $a->_pos, $a->_title, $a->_body, $a->_append, $this->_id, $a->_aid); + $a->cid, $a->pos, $a->title, $a->body, $a->append, $this->id, $a->aid); } else { + // New article XDB::startTransaction(); list($aid, $pos) = XDB::fetchOneRow('SELECT MAX(aid) AS aid, MAX(pos) AS pos FROM newsletter_art AS a WHERE a.id = {?}', - $this->_id); - $a->_aid = ++$aid; - $a->_pos = ($a->_pos ? $a->_pos : ++$pos); + $this->id); + $a->aid = ++$aid; + $a->pos = ($a->pos ? $a->pos : ++$pos); XDB::execute('INSERT INTO newsletter_art (id, aid, cid, pos, title, body, append) VALUES ({?}, {?}, {?}, {?}, {?}, {?}, {?})', - $this->_id, $a->_aid, $a->_cid, $a->_pos, - $a->_title, $a->_body, $a->_append); + $this->id, $a->aid, $a->cid, $a->pos, + $a->title, $a->body, $a->append); XDB::commit(); } - $this->_arts['a' . $a->_aid] = $a; + // Update local ID of article + $this->arts[$a->aid] = $a; } + /** Delete an article by its ID + * @p $aid ID of the article to delete + */ public function delArticle($aid) { - XDB::execute('DELETE FROM newsletter_art WHERE id={?} AND aid={?}', $this->_id, $aid); - foreach ($this->_arts as $key=>$art) { - unset($this->_arts[$key]["a$aid"]); + $this->fetchArticles(); + + XDB::execute('DELETE FROM newsletter_art WHERE id={?} AND aid={?}', $this->id, $aid); + foreach ($this->arts as $key=>$art) { + unset($this->arts[$key][$aid]); } } - protected function assignData(&$smarty) + // }}} + // {{{ Display + + /** Retrieve the title of this issue + * @p $mail Whether we want the normal title or the email subject + * @return Title of the issue + */ + public function title($mail = false) { - $smarty->assign_by_ref('nl', $this); + return $mail ? $this->title_mail : $this->title; } - protected function setSent() + /** Retrieve the head of this issue + * @p $user User for customization (may be null: no customization) + * @p $type Either 'text' or 'html' + * @return Formatted head of the issue. + */ + public function head($user = null, $type = 'text') { - XDB::execute("UPDATE newsletter SET bits='sent' WHERE id={?}", $this->_id); + if (is_null($user)) { + return $this->head; + } else { + $head = $this->head; + $head = str_replace(array('', '', ''), + array(($user->isFemale() ? 'Chère' : 'Cher'), $user->displayName(), ''), + $head); + return format_text($head, $type, 2, 64); + } } - static public function subscriptionState($uid = null) + /** Retrieve the formatted signature of this issue. + */ + public function signature($type = 'text') { - $user = is_null($uid) ? S::v('uid') : $uid; - $res = XDB::query("SELECT 1 - FROM newsletter_ins - WHERE uid={?}", $user); - return $res->fetchOneCell(); + return format_text($this->signature, $type, 2, 64); + } + + /** Get the title of a given category + * @p $cid ID of the category to retrieve + * @return Name of the category + */ + public function category($cid) + { + return $this->nl->cats[$cid]; + } + + /** Add required data to the given $page for proper CSS display + * @p $page Smarty object + * @return Either 'true' (if CSS was added to a page) or the raw CSS to add (when $page is null) + */ + public function css(&$page = null) + { + if (!is_null($page)) { + $page->addCssLink($this->nl->cssFile()); + return true; + } else { + $css = file_get_contents(dirname(__FILE__) . '/../htdocs/css/' . $this->nl->cssFile()); + return preg_replace('@/\*.*?\*/@us', '', $css); + } + } + + /** Set up a smarty page for a 'text' mode render of the issue + * @p $page Smarty object (using the $this->nl->tplFile() template) + * @p $user User to use when rendering the template + */ + public function toText(&$page, $user) + { + $this->fetchArticles(); + + $this->css($page); + $page->assign('prefix', null); + $page->assign('is_mail', false); + $page->assign('mail_part', 'text'); + $page->assign('user', $user); + $page->assign('hash', null); + $this->assignData($page); } - static public function unsubscribe($uid = null) + /** Set up a smarty page for a 'html' mode render of the issue + * @p $page Smarty object (using the $this->nl->tplFile() template) + * @p $user User to use when rendering the template + */ + public function toHtml(&$page, $user) { - $user = is_null($uid) ? S::v('uid') : $uid; - XDB::execute("DELETE FROM newsletter_ins - WHERE uid={?}", $user); + $this->fetchArticles(); + + $this->css($page); + $page->assign('prefix', $this->nl->prefix() . '/show/' . $this->id()); + $page->assign('is_mail', false); + $page->assign('mail_part', 'html'); + $page->assign('user', $user); + $page->assign('hash', null); + $this->assignData($page); } - static public function subscribe($uid = null) + /** Set all 'common' data for the page (those which are required for both web and email rendering) + * @p $smarty Smarty object (e.g page) which should be filled + */ + protected function assignData(&$smarty) { - $user = is_null($uid) ? S::v('uid') : $uid; - XDB::execute('INSERT IGNORE INTO newsletter_ins (uid, last, hash) - VALUES ({?}, NULL, NULL)', $user); + $this->fetchArticles(); + + $smarty->assign_by_ref('issue', $this); + $smarty->assign_by_ref('nl', $this->nl); } - protected function subscriptionWhere() + // }}} + // {{{ Mailing + + /** Retrieve the 'Send before' date, in a clean format. + */ + public function getSendBeforeDate() { - return '1'; + return strftime('%Y-%m-%d', strtotime($this->send_before)); } - static public function create() + /** Retrieve the 'Send before' time (i.e hour), in a clean format. + */ + public function getSendBeforeTime() { - XDB::execute("INSERT INTO newsletter - SET bits='new',date=NOW(),titre='to be continued',titre_mail='to be continued'"); + return strtotime($this->send_before); } - static public function listSent() + /** Create a hash based on some additional data + * $line Line-specific data (to prevent two hashes generated at the same time to be the same) + */ + protected static function createHash($line) { - $res = XDB::query("SELECT IF(short_name IS NULL, id,short_name) as id,date,titre_mail AS titre - FROM newsletter - WHERE bits!='new' - ORDER BY date DESC"); - return $res->fetchAllAssoc(); + $hash = implode(time(), $line) . rand(); + $hash = md5($hash); + return $hash; } - static public function listAll() + /** Send this issue to the given user, reusing an existing hash if provided. + * @p $user User to whom the issue should be mailed + * @p $hash Optional hash to use in the 'unsubscribe' link; if null, another one will be generated. + */ + public function sendTo($user, $hash = null) { - $res = XDB::query("SELECT IF(short_name IS NULL, id,short_name) as id,date,titre_mail AS titre - FROM newsletter - ORDER BY date DESC"); - return $res->fetchAllAssoc(); + $this->fetchArticles(); + + if (is_null($hash)) { + $hash = XDB::fetchOneCell("SELECT hash + FROM newsletter_ins + WHERE uid = {?} AND nlid = {?}", + $user->id(), $this->nl->id); + } + if (is_null($hash)) { + $hash = self::createHash(array($user->displayName(), $user->fullName(), + $user->isFemale(), $user->isEmailFormatHtml(), + rand(), "X.org rulez")); + XDB::execute("UPDATE newsletter_ins as ni + SET ni.hash = {?} + WHERE ni.uid = {?} AND ni.nlid = {?}", + $hash, $user->id(), $this->nl->id); + } + + $mailer = new PlMailer($this->nl->tplFile()); + $this->assignData($mailer); + $mailer->assign('is_mail', true); + $mailer->assign('user', $user); + $mailer->assign('prefix', null); + $mailer->assign('hash', $hash); + $mailer->sendTo($user); } + + /** Select a subset of subscribers which should receive the newsletter. + * NL-Specific selections (not yet received, is subscribed) are done when sending. + * @return A PlFilterCondition. + */ + protected function getRecipientsUFC() + { + return $this->sufb->getUFC(); + } + + /** Check whether a given user may see this issue. + * @p $user User whose access should be checked + * @return Whether he may access the issue + */ + public function checkUser($user = null) + { + if ($user == null) { + $user = S::user(); + } + $uf = new UserFilter($this->getRecipientsUFC()); + return $uf->checkUser($user); + } + + /** Sent this issue to all valid recipients + * @return Number of issues sent + */ + public function sendToAll() + { + $this->fetchArticles(); + + XDB::execute('UPDATE newsletter_issues + SET state = \'sent\', date=CURDATE() + 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, new PlLimit(self::BATCH_SIZE)); + + while (true) { + $sent = array(); + $users = $uf->getUsers(); + if (count($users) == 0) { + return $emailsCount; + } + foreach ($users as $user) { + $sent[] = $user->id(); + $this->sendTo($user, $hash); + ++$emailsCount; + } + XDB::execute("UPDATE newsletter_ins + SET last = {?} + WHERE nlid = {?} AND uid IN {?}", $this->id, $this->nl->id, $sent); + + sleep(60); + } + return $emailsCount; + } + + // }}} } // }}} @@ -190,45 +1105,48 @@ class NewsLetter extends MassMailer class NLArticle { + // Maximum number of lines per article + const MAX_LINES_PER_ARTICLE = 9; + // {{{ properties - var $_aid; - var $_cid; - var $_pos; - var $_title; - var $_body; - var $_append; + public $aid; + public $cid; + public $pos; + public $title; + public $body; + public $append; // }}} // {{{ constructor function __construct($title='', $body='', $append='', $aid=-1, $cid=0, $pos=0) { - $this->_body = $body; - $this->_title = $title; - $this->_append = $append; - $this->_aid = $aid; - $this->_cid = $cid; - $this->_pos = $pos; + $this->body = $body; + $this->title = $title; + $this->append = $append; + $this->aid = $aid; + $this->cid = $cid; + $this->pos = $pos; } // }}} // {{{ function title() public function title() - { return trim($this->_title); } + { return trim($this->title); } // }}} // {{{ function body() public function body() - { return trim($this->_body); } + { return trim($this->body); } // }}} // {{{ function append() public function append() - { return trim($this->_append); } + { return trim($this->append); } // }}} // {{{ function toText() @@ -236,8 +1154,8 @@ class NLArticle public function toText($hash = null, $login = null) { $title = '*'.$this->title().'*'; - $body = MiniWiki::WikiToText($this->_body, true); - $app = MiniWiki::WikiToText($this->_append,false,4); + $body = MiniWiki::WikiToText($this->body, true); + $app = MiniWiki::WikiToText($this->append, false, 4); $text = trim("$title\n\n$body\n\n$app")."\n"; if (!is_null($hash) && !is_null($login)) { $text = str_replace('%HASH%', "$hash/$login", $text); @@ -252,9 +1170,9 @@ class NLArticle public function toHtml($hash = null, $login = null) { - $title = "

".pl_entities($this->title()).'

'; - $body = MiniWiki::WikiToHTML($this->_body); - $app = MiniWiki::WikiToHTML($this->_append); + $title = "

".pl_entities($this->title()).'

'; + $body = MiniWiki::WikiToHTML($this->body); + $app = MiniWiki::WikiToHTML($this->append); $art = "$title\n"; $art .= "
\n$body\n"; @@ -276,7 +1194,7 @@ class NLArticle public function check() { - $text = MiniWiki::WikiToText($this->_body); + $text = MiniWiki::WikiToText($this->body); $arr = explode("\n",wordwrap($text,68)); $c = 0; foreach ($arr as $line) { @@ -284,13 +1202,13 @@ class NLArticle $c++; } } - return $c<9; + return $c < self::MAX_LINES_PER_ARTICLE; } // }}} // {{{ function parseUrlsFromArticle() - private function parseUrlsFromArticle() + protected function parseUrlsFromArticle() { $email_regex = '([a-z0-9.\-+_\$]+@([\-.+_]?[a-z0-9])+)'; $url_regex = '((https?|ftp)://[a-zA-Z0-9._%#+/?=&~-]+)'; @@ -346,5 +1264,19 @@ class NLArticle // }}} +// {{{ Functions + +function format_text($input, $format, $indent = 0, $width = 68) +{ + if ($format == 'text') { + return MiniWiki::WikiToText($input, true, $indent, $width, "title"); + } + return MiniWiki::WikiToHTML($input, "title"); +} + +// function enriched_to_text($input,$html=false,$just=false,$indent=0,$width=68) + +// }}} + // vim:set et sw=4 sts=4 sws=4 enc=utf-8: ?> diff --git a/include/validations/nl.inc.php b/include/validations/nl.inc.php index ef07d35..0a385ac 100644 --- a/include/validations/nl.inc.php +++ b/include/validations/nl.inc.php @@ -60,9 +60,9 @@ class NLReq extends Validate protected function handle_editor() { - $this->art->_body = Env::v('nl_body'); - $this->art->_title = Env::v('nl_title'); - $this->art->_append = Env::v('nl_append'); + $this->art->body = Env::v('nl_body'); + $this->art->title = Env::v('nl_title'); + $this->art->append = Env::v('nl_append'); return true; } @@ -106,7 +106,7 @@ class NLReq extends Validate public function commit() { - $nl = new NewsLetter(); + $nl = NewsLetter::forGroup(NewsLetter::GROUP_XORG)->getPendingIssue(); $nl->saveArticle($this->art); return true; } diff --git a/modules/admin.php b/modules/admin.php index d4fb1ed..ac2b4ed 100644 --- a/modules/admin.php +++ b/modules/admin.php @@ -1075,7 +1075,7 @@ class AdminModule extends PLModule { $page->changeTpl('admin/validation.tpl'); $page->setTitle('Administration - Valider une demande'); - $page->addCssLink('nl.css'); + $page->addCssLink('nl.Polytechnique.org.css'); if ($action == 'edit' && !is_null($id)) { $page->assign('preview_id', $id); diff --git a/modules/axletter.php b/modules/axletter.php index 67bdf55..9467dd0 100644 --- a/modules/axletter.php +++ b/modules/axletter.php @@ -19,366 +19,38 @@ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ***************************************************************************/ -class AXLetterModule extends PLModule +Platal::load('newsletter'); + +class AXLetterModule extends NewsletterModule { function handlers() { return array( - 'ax' => $this->make_hook('index', AUTH_COOKIE), - 'ax/out' => $this->make_hook('out', AUTH_PUBLIC), - 'ax/show' => $this->make_hook('show', AUTH_COOKIE), - 'ax/edit' => $this->make_hook('submit', AUTH_MDP), - 'ax/edit/cancel' => $this->make_hook('cancel', AUTH_MDP), - 'ax/edit/valid' => $this->make_hook('valid', AUTH_MDP), - 'admin/axletter' => $this->make_hook('admin', AUTH_MDP, 'admin'), + 'ax' => $this->make_hook('nl', AUTH_COOKIE), + 'ax/out' => $this->make_hook('out', AUTH_PUBLIC), + 'ax/show' => $this->make_hook('nl_show', AUTH_COOKIE), + 'ax/admin' => $this->make_hook('admin_nl', AUTH_MDP), + 'ax/admin/edit' => $this->make_hook('admin_nl_edit', AUTH_MDP), + 'ax/admin/edit/valid' => $this->make_hook('admin_nl_valid', AUTH_MDP), + 'ax/admin/edit/cancel' => $this->make_hook('admin_nl_cancel', AUTH_MDP), + 'ax/admin/edit/delete' => $this->make_hook('admin_nl_delete', AUTH_MDP), ); } + protected function getNl() + { + require_once 'newsletter.inc.php'; + return NewsLetter::forGroup(NewsLetter::GROUP_AX); + } + function handler_out(&$page, $hash = null) { if (!$hash) { if (!S::logged()) { return PL_DO_AUTH; - } else { - return $this->handler_index($page, 'out'); - } - } - $this->load('axletter.inc.php'); - $page->changeTpl('axletter/unsubscribe.tpl'); - $page->assign('success', AXLetter::unsubscribe($hash, true)); - } - - function handler_index(&$page, $action = null) - { - $this->load('axletter.inc.php'); - - $page->changeTpl('axletter/index.tpl'); - $page->setTitle('Envois de l\'AX'); - - switch ($action) { - case 'in': AXLetter::subscribe(); break; - case 'out': AXLetter::unsubscribe(); break; - } - - $perm = AXLetter::hasPerms(); - if ($perm) { - $res = XDB::query("SELECT * FROM axletter_ins"); - $page->assign('count', $res->numRows()); - $page->assign('new', AXLetter::awaiting()); - } - $page->assign('axs', AXLetter::subscriptionState()); - $page->assign('ax_list', AXLetter::listSent()); - $page->assign('ax_rights', $perm); - } - - function handler_submit(&$page, $action = null) - { - $this->load('axletter.inc.php'); - if (!AXLetter::hasPerms()) { - return PL_FORBIDDEN; - } - - $page->changeTpl('axletter/edit.tpl'); - - $saved = Post::i('saved'); - $new = false; - $id = Post::i('id'); - $short_name = trim(Post::v('short_name')); - $subject = trim(Post::v('subject')); - $title = trim(Post::v('title')); - $body = rtrim(Post::v('body')); - $signature = trim(Post::v('signature')); - $promo_min = Post::i('promo_min'); - $promo_max = Post::i('promo_max'); - $subset_to = preg_split("/[ ,;\:\n\r]+/", Post::v('subset_to'), -1, PREG_SPLIT_NO_EMPTY); - $subset = (count($subset_to) > 0); - $subset_rm = Post::b('subset_rm'); - $echeance = Post::has('echeance_date') ? - preg_replace('/^(\d\d\d\d)(\d\d)(\d\d)$/', '\1-\2-\3', Post::v('echeance_date')) . ' ' . Post::v('echeance_time') - : Post::v('echeance'); - $echeance_date = Post::v('echeance_date'); - $echeance_time = Post::v('echeance_time'); - - if (!$id) { - $res = XDB::query("SELECT * FROM axletter WHERE FIND_IN_SET('new', bits)"); - if ($res->numRows()) { - extract($res->fetchOneAssoc(), EXTR_OVERWRITE); - $subset_to = ($subset ? explode("\n", $subset) : array()); - $subset = (count($subset_to) > 0); - $saved = true; - } else { - XDB::execute("INSERT INTO axletter SET id = NULL"); - $id = XDB::insertId(); - } - if (!$echeance || $echeance == '0000-00-00 00:00:00') { - $saved = false; - $new = true; - } - } elseif (Post::has('valid')) { - S::assert_xsrf_token(); - - if (!$subject && $title) { - $subject = $title; - } - if (!$title && $subject) { - $title = $subject; - } - if (!$subject || !$title || !$body) { - $page->trigError("L'article doit avoir un sujet et un contenu"); - Post::kill('valid'); - } - if (($promo_min > $promo_max && $promo_max != 0)|| - ($promo_min != 0 && ($promo_min <= 1900 || $promo_min >= 2020)) || - ($promo_max != 0 && ($promo_max <= 1900 || $promo_max >= 2020))) - { - $page->trigError("L'intervalle de promotions n'est pas valide"); - Post::kill('valid'); - } - if (empty($short_name)) { - $page->trigError("L'annonce doit avoir un nom raccourci pour simplifier la navigation dans les archives"); - Post::kill('valid'); - } elseif (!preg_match('/^[a-z][-a-z0-9]*[a-z0-9]$/', $short_name)) { - $page->trigError("Le nom raccourci n'est pas valide, il doit comporter au moins 2 caractères et n'être composé " - . "que de chiffres, lettres et tirets"); - Post::kill('valid'); - } elseif ($short_name != Post::v('old_short_name')) { - $res = XDB::query("SELECT id FROM axletter WHERE short_name = {?}", $short_name); - if ($res->numRows() && $res->fetchOneCell() != $id) { - $page->trigError("Le nom $short_name est déjà utilisé, merci d'en choisir un autre"); - $short_name = Post::v('old_short_name'); - if (empty($short_name)) { - Post::kill('valid'); - } - } - } - - switch (@Post::v('valid')) { - case 'Vérifier les emails': - // Same as 'preview', but performs a test of all provided emails - if ($subset) { - require_once 'emails.inc.php'; - $ids = ids_from_mails($subset_to); - $nb_error = 0; - foreach ($subset_to as $e) { - if (!array_key_exists($e, $ids)) { - if ($nb_error == 0) { - $page->trigError("Emails inconnus :"); - } - $nb_error++; - $page->trigError($e); - } - } - if ($nb_error == 0) { - if (count($subset_to) == 1) { - $page->trigSuccess("L'email soumis a été reconnu avec succès."); - } else { - $page->trigSuccess("Les " . count($subset_to) . " emails soumis ont été reconnus avec succès."); - } - } else { - $page->trigError("Total : $nb_error erreur" . ($nb_error > 1 ? "s" : "") . " sur " . count($subset_to) . " adresses mail soumises."); - } - $page->trigSuccess("Les adresses soumises correspondent à un total de " . count(array_unique($ids)) . " camarades."); - } - // No break here, since Vérifier is a subcase of Aperçu. - case 'Aperçu': - $this->load('axletter.inc.php'); - $al = new AXLetter(array($id, $short_name, $subject, $title, $body, $signature, - $promo_min, $promo_max, $subset, $subset_rm, $echeance, 0, 'new')); - $al->toHtml($page, S::user()); - break; - - case 'Confirmer': - XDB::execute('INSERT INTO axletter (id, short_name, subject, title, body, signature, - promo_min, promo_max, echeance, subset, subset_rm) - VALUES ({?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}, {?}) - ON DUPLICATE KEY UPDATE short_name = VALUES(short_name), subject = VALUES(subject), title = VALUES(title), - body = VALUES(body), signature = VALUES(signature), promo_min = VALUES(promo_min), - promo_max = VALUES(promo_max), echeance = VALUES(echeance), subset = VALUES(subset), - subset_rm = VALUES(subset_rm)', - $id, $short_name, $subject, $title, $body, $signature, $promo_min, $promo_max, $echeance, - $subset ? implode("\n", $subset_to) : null, $subset_rm); - if (!$saved) { - global $globals; - $mailer = new PlMailer(); - $mailer->setFrom("support@" . $globals->mail->domain); - $mailer->setSubject("Un nouveau projet d'email de l'AX vient d'être proposé"); - $mailer->setTxtBody("Un nouvel email vient d'être rédigé en prévision d'un envoi prochain. Vous pouvez " - . "le modifier jusqu'à ce qu'il soit verrouillé pour l'envoi\n\n" - . "Le sujet de l'email : $subject\n" - . "L'échéance d'envoi est fixée à $echeance.\n" - . "L'email pourra néanmoins partir avant cette échéance si un administrateur de " - . "Polytechnique.org le valide.\n\n" - . "Pour modifier, valider ou annuler l'email :\n" - . "https://www.polytechnique.org/ax/edit\n" - . "-- \n" - . "Association Polytechnique.org\n"); - $users = User::getBulkUsersWithUIDs(XDB::fetchColumn('SELECT uid - FROM axletter_rights')); - foreach ($users as $user) { - $mailer->addTo($user); - } - $mailer->send(); - } - $saved = true; - $echeance_date = null; - $echeance_time = null; - pl_redirect('ax'); - break; - } - } - $page->assign('id', $id); - $page->assign('short_name', $short_name); - $page->assign('subject', $subject); - $page->assign('title', $title); - $page->assign('body', $body); - $page->assign('signature', $signature); - $page->assign('promo_min', $promo_min); - $page->assign('promo_max', $promo_max); - $page->assign('subset_to', implode("\n", $subset_to)); - $page->assign('subset', $subset); - $page->assign('subset_rm', $subset_rm); - $page->assign('echeance', $echeance); - $page->assign('echeance_date', $echeance_date); - $page->assign('echeance_time', $echeance_time); - $page->assign('saved', $saved); - $page->assign('new', $new); - $page->assign('is_xorg', S::admin()); - - if (!$saved) { - $select = ''; - for ($i = 0 ; $i < 24 ; $i++) { - $stamp = sprintf('%02d:00:00', $i); - if ($stamp == $echeance_time) { - $sel = ' selected="selected"'; - } else { - $sel = ''; - } - $select .= "\n"; - } - $page->assign('echeance_time', $select); - } - } - - function handler_cancel(&$page, $force = null) - { - $this->load('axletter.inc.php'); - if (!AXLetter::hasPerms() || !S::has_xsrf_token()) { - return PL_FORBIDDEN; - } - - $al = AXLetter::awaiting(); - if (!$al) { - $page->kill("Aucune lettre en attente"); - return; - } - if (!$al->invalid()) { - $page->kill("Une erreur est survenue lors de l'annulation de l'envoi"); - return; - } - - $page->killSuccess("L'envoi de l'annonce {$al->title()} est annulé."); - } - - function handler_valid(&$page, $force = null) - { - $this->load('axletter.inc.php'); - if (!AXLetter::hasPerms() || !S::has_xsrf_token()) { - return PL_FORBIDDEN; - } - - $al = AXLetter::awaiting(); - if (!$al) { - $page->kill("Aucune lettre en attente"); - return; - } - if (!$al->valid()) { - $page->kill("Une erreur est survenue lors de la validation de l'envoi"); - return; - } - - $page->killSuccess("L'envoi de l'annonce aura lieu dans l'heure qui vient."); - } - - function handler_show(&$page, $nid = 'last') - { - $this->load('axletter.inc.php'); - $page->changeTpl('axletter/show.tpl'); - - try { - $nl = new AXLetter($nid); - $user =& S::user(); - if (Get::has('text')) { - $nl->toText($page, $user); - } else { - $nl->toHtml($page, $user); - } - if (Post::has('send')) { - $nl->sendTo($user); - } - } catch (MailNotFound $e) { - return PL_NOT_FOUND; - } - } - - function handler_admin(&$page, $action = null, $uid = null) - { - $this->load('axletter.inc.php'); - if (Post::has('action')) { - $action = Post::v('action'); - $uid = Post::v('uid'); - } - if ($uid) { - S::assert_xsrf_token(); - - $uids = preg_split('/ *[,;\: ] */', $uid); - foreach ($uids as $uid) { - switch ($action) { - case 'add': - $res = AXLetter::grantPerms($uid); - break; - case 'del'; - $res = AXLetter::revokePerms($uid); - break; - } - if (!$res) { - $page->trigError("Personne ne correspond à l'identifiant '$uid'"); - } - } - } - - $page->changeTpl('axletter/admin.tpl'); - $page->assign('admins', User::getBulkUsersWithUIDs(XDB::fetchColumn('SELECT uid - FROM axletter_rights'))); - - $importer = new CSVImporter('axletter_ins'); - $importer->registerFunction('uid', 'email vers Id X.org', array($this, 'idFromMail')); - $importer->forceValue('hash', array($this, 'createHash')); - $importer->apply($page, "admin/axletter", array('uid', 'email', 'prenom', 'nom', 'promo', 'flag', 'hash')); - } - - function idFromMail($line, $key, $relation = null) - { - static $field; - global $globals; - if (!isset($field)) { - $field = array('email', 'mail', 'login', 'bestalias', 'forlife', 'flag'); - foreach ($field as $fld) { - if (isset($line[$fld])) { - $field = $fld; - break; - } } } - $uf = new UserFilter(new UFC_Email($line[$field])); - $id = $uf->getUIDs(); - return count($id) == 1 ? $id[0] : 0; - } - - function createHash($line, $key, $relation) - { - $hash = implode(time(), $line) . rand(); - $hash = md5($hash); - return $hash; + return $this->handler_nl($page, 'out', $hash); } } diff --git a/modules/axletter/axletter.inc.php b/modules/axletter/axletter.inc.php deleted file mode 100644 index 1d1a52a..0000000 --- a/modules/axletter/axletter.inc.php +++ /dev/null @@ -1,245 +0,0 @@ -_head = ' ,'; - - if (!is_array($id)) { - if ($id == 'last') { - $res = XDB::query("SELECT * - FROM axletter - WHERE FIND_IN_SET('sent', bits) - ORDER BY id DESC"); - } else { - $res = XDB::query("SELECT * - FROM axletter - WHERE id = {?} OR short_name = {?}", $id, $id); - } - if (!$res->numRows()) { - throw new MailNotFound(); - } - $id = $res->fetchOneRow(); - } - list($this->_id, $this->_shortname, $this->_title_mail, $this->_title, - $this->_body, $this->_signature, $this->_promo_min, $this->_promo_max, - $this->_subset_to, $this->_subset_rm, $this->_echeance, $this->_date, $this->_bits) = $id; - if ($this->_date == '0000-00-00') { - $this->_date = 0; - } - $this->_subset_to = ($this->_subset_to ? explode("\n", $this->_subset_to) : null); - $this->_subset = (count($this->_subset_to) > 0); - } - - protected function assignData(&$smarty) - { - $smarty->assign_by_ref('am', $this); - } - - public function body($format) - { - return format_text($this->_body, $format); - } - - public function signature($format) - { - return format_text($this->_signature, $format, 10); - } - - public function valid() - { - return XDB::execute("UPDATE axletter - SET echeance = NOW() - WHERE id = {?}", $this->_id); - } - - public function invalid() - { - return XDB::execute("UPDATE axletter - SET bits = 'invalid', date = CURDATE() - WHERE id = {?}", $this->_id); - } - - protected function setSent() - { - XDB::execute("UPDATE axletter - SET bits='sent', date=CURDATE() - WHERE id={?}", $this->_id); - } - - static public function subscriptionState($uid = null) - { - $user = is_null($uid) ? S::v('uid') : $uid; - $res = XDB::query("SELECT 1 - FROM axletter_ins - WHERE uid={?}", $user); - return $res->fetchOneCell(); - } - - static public function unsubscribe($uid = null, $hash = false) - { - $user = is_null($uid) ? S::v('uid') : $uid; - $field = !$hash ? 'uid' : 'hash'; - if (is_null($uid) && $hash) { - return false; - } - $res = XDB::query("SELECT uid - FROM axletter_ins - WHERE $field={?}", $user); - if ($res->numRows() != 1) { - return false; - } - XDB::execute("DELETE FROM axletter_ins - WHERE $field = {?}", $user); - return true; - } - - static public function subscribe($uid = null) - { - $user = is_null($uid) ? S::v('uid') : $uid; - XDB::execute('INSERT IGNORE INTO axletter_ins (uid, last) - VALUES ({?}, 0)', $user); - } - - static public function hasPerms() - { - if (S::admin()) { - return true; - } - $res = XDB::query("SELECT COUNT(*) - FROM axletter_rights - WHERE uid = {?}", S::i('uid')); - return ($res->fetchOneCell() > 0); - } - - static public function grantPerms($uid) - { - if (!is_numeric($uid)) { - $res = XDB::query("SELECT uid FROM aliases WHERE alias = {?}", $uid); - $uid = $res->fetchOneCell(); - } - if (!$uid) { - return false; - } - return XDB::execute("INSERT IGNORE INTO axletter_rights SET uid = {?}", $uid); - } - - static public function revokePerms($uid) - { - if (!is_numeric($uid)) { - $res = XDB::query("SELECT uid FROM aliases WHERE alias = {?}", $uid); - $uid = $res->fetchOneCell(); - } - if (!$uid) { - return false; - } - return XDB::execute("DELETE FROM axletter_rights WHERE uid = {?}", $uid); - } - - protected function subscriptionWhere() - { - if (!$this->_promo_min && !$this->_promo_max && !$this->_subset) { - return '1'; - } - /* TODO: refines this filter on promotions by using userfilter. */ - $where = array(); - if ($this->_promo_min) { - $where[] = "((ni.uid = 0 AND ni.promo >= {$this->_promo_min}) OR (ni.uid != 0 AND pd.promo >= 'X{$this->_promo_min}'))"; - } - if ($this->_promo_max) { - $where[] = "((ni.uid = 0 AND ni.promo <= {$this->_promo_max}) OR (ni.uid != 0 AND pd.promo <= 'X{$this->_promo_max}'))"; - } - if ($this->_subset) { - require_once("emails.inc.php"); - $ids = ids_from_mails($this->_subset_to); - $ids_list = implode(',', $ids); - if(count($ids) > 0) { - if ($this->_subset_rm) { - $where[] = "ni.uid NOT IN ($ids_list)"; - } else { - $where[] = "ni.uid IN ($ids_list)"; - } - } else { - // No valid email - $where[] = "0"; - } - } - return implode(' AND ', $where); - } - - static public function awaiting() - { - $res = XDB::query("SELECT * - FROM axletter - WHERE FIND_IN_SET('new', bits)"); - if ($res->numRows()) { - return new AXLetter($res->fetchOneRow()); - } - return null; - } - - static public function toSend() - { - $res = XDB::query("SELECT * - FROM axletter - WHERE FIND_IN_SET('new', bits) AND echeance <= NOW() AND echeance != 0"); - if ($res->numRows()) { - return new AXLetter($res->fetchOneRow()); - } - return null; - } - - static public function listSent() - { - $res = XDB::query("SELECT IF(short_name IS NULL, id, short_name) as id, date, subject AS titre - FROM axletter - WHERE NOT FIND_IN_SET('new', bits) AND NOT FIND_IN_SET('invalid', bits) - ORDER BY date DESC"); - return $res->fetchAllAssoc(); - } - - static public function listAll() - { - $res = XDB::query("SELECT IF(short_name IS NULL, id, short_name) as id, date, subject AS titre - FROM axletter - ORDER BY date DESC"); - return $res->fetchAllAssoc(); - } -} - -// vim:set et sw=4 sts=4 sws=4 enc=utf-8: -?> diff --git a/modules/newsletter.php b/modules/newsletter.php index 051b4ed..0b84a38 100644 --- a/modules/newsletter.php +++ b/modules/newsletter.php @@ -24,48 +24,67 @@ class NewsletterModule extends PLModule function handlers() { return array( - 'nl' => $this->make_hook('nl', AUTH_COOKIE), - 'nl/show' => $this->make_hook('nl_show', AUTH_COOKIE), - 'nl/submit' => $this->make_hook('nl_submit', AUTH_MDP), - 'admin/newsletter' => $this->make_hook('admin_nl', AUTH_MDP, 'admin'), - 'admin/newsletter/categories' => $this->make_hook('admin_nl_cat', AUTH_MDP, 'admin'), - 'admin/newsletter/edit' => $this->make_hook('admin_nl_edit', AUTH_MDP, 'admin'), + 'nl' => $this->make_hook('nl', AUTH_COOKIE), + 'nl/show' => $this->make_hook('nl_show', AUTH_COOKIE), + 'nl/submit' => $this->make_hook('nl_submit', AUTH_MDP), + 'admin/nls' => $this->make_hook('admin_nl_groups', AUTH_MDP, 'admin'), + 'admin/newsletter' => $this->make_hook('admin_nl', AUTH_MDP, 'admin'), + 'admin/newsletter/categories' => $this->make_hook('admin_nl_cat', AUTH_MDP, 'admin'), + 'admin/newsletter/edit' => $this->make_hook('admin_nl_edit', AUTH_MDP, 'admin'), + 'admin/newsletter/edit/delete' => $this->make_hook('delete', AUTH_MDP, 'admin'), + // Automatic mailing is disabled for X.org NL +// 'admin/newsletter/edit/cancel' => $this->make_hook('cancel', AUTH_MDP, 'admin'), +// 'admin/newsletter/edit/valid' => $this->make_hook('valid', AUTH_MDP, 'admin'), ); } - function handler_nl(&$page, $action = null) + /** This function should return the adequate NewsLetter object for the current module. + */ + protected function getNl() { require_once 'newsletter.inc.php'; + return NewsLetter::forGroup(NewsLetter::GROUP_XORG); + } + + function handler_nl(&$page, $action = null, $hash = null) + { + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } $page->changeTpl('newsletter/index.tpl'); $page->setTitle('Lettres mensuelles'); switch ($action) { - case 'out': NewsLetter::unsubscribe(); break; - case 'in': NewsLetter::subscribe(); break; + case 'out': $nl->unsubscribe($hash, $hash != null); break; + case 'in': $nl->subscribe(); break; default: ; } - $page->assign('nls', NewsLetter::subscriptionState()); - $page->assign('nl_list', NewsLetter::listSent()); + $page->assign_by_ref('nl', $nl); + $page->assign('nls', $nl->subscriptionState()); + $page->assign('nl_list', $nl->listSentIssues(true)); } function handler_nl_show(&$page, $nid = 'last') { $page->changeTpl('newsletter/show.tpl'); - - require_once 'newsletter.inc.php'; + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } try { - $nl = new NewsLetter($nid); + $issue = $nl->getIssue($nid); $user =& S::user(); if (Get::has('text')) { - $nl->toText($page, $user); + $issue->toText($page, $user); } else { - $nl->toHtml($page, $user); + $issue->toHtml($page, $user); } if (Post::has('send')) { - $nl->sendTo($user); + $issue->sendTo($user); } } catch (MailNotFound $e) { return PL_NOT_FOUND; @@ -76,7 +95,11 @@ class NewsletterModule extends PLModule { $page->changeTpl('newsletter/submit.tpl'); - require_once 'newsletter.inc.php'; + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } + $wp = new PlWikiPage('Xorg.LettreMensuelle'); $wp->buildCache(); @@ -92,63 +115,112 @@ class NewsletterModule extends PLModule $art->submit(); $page->assign('submited', true); } - $page->addCssLink('nl.css'); + $page->addCssLink($nl->cssFile()); } function handler_admin_nl(&$page, $new = false) { $page->changeTpl('newsletter/admin.tpl'); $page->setTitle('Administration - Newsletter : liste'); - require_once("newsletter.inc.php"); + + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } if($new) { - NewsLetter::create(); - pl_redirect("admin/newsletter"); + $id = $nl->createPending(); + pl_redirect($nl->adminPrefix() . '/edit/' . $id); } - $page->assign('nl_list', NewsLetter::listAll()); + $page->assign_by_ref('nl', $nl); + $page->assign('nl_list', $nl->listAllIssues()); + } + + function handler_admin_nl_groups(&$page) + { + require_once 'newsletter.inc.php'; + + $page->changeTpl('newsletter/admin_all.tpl'); + $page->setTitle('Administration - Newsletters : Liste des Newsletters'); + + $page->assign('nls', Newsletter::getAll()); } function handler_admin_nl_edit(&$page, $nid = 'last', $aid = null, $action = 'edit') { $page->changeTpl('newsletter/edit.tpl'); - $page->addCssLink('nl.css'); + $page->addCssLink('nl.Polytechnique.org.css'); $page->setTitle('Administration - Newsletter : Édition'); - require_once 'newsletter.inc.php'; - $nl = new NewsLetter($nid); + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } - if($action == 'delete') { - $nl->delArticle($aid); - pl_redirect("admin/newsletter/edit/$nid"); + try { + $issue = $nl->getIssue($nid, false); + } catch (MailNotFound $e) { + return PL_NOT_FOUND; } + $ufb = $nl->getSubscribersUFB(); + $ufb_keepenv = false; // Will be set to True if there were invalid modification to the UFB. + + // Convert NLIssue error messages to human-readable errors + $error_msgs = array( + NLIssue::ERROR_INVALID_SHORTNAME => "Le nom court est invalide ou vide.", + NLIssue::ERROR_INVALID_UFC => "Le filtre des destinataires est invalide.", + NLIssue::ERROR_SQL_SAVE => "Une erreur est survenue en tentant de sauvegarder la lettre, merci de réessayer.", + ); + + // Update the current issue if($aid == 'update') { - $nl->_title = Post::v('title'); - $nl->_title_mail = Post::v('title_mail'); - $nl->_date = Post::v('date'); - $nl->_head = Post::v('head'); - $nl->_shortname = strlen(Post::v('shortname')) ? Post::v('shortname') : null; - if (preg_match('/^[-a-z0-9]*$/i', $nl->_shortname) && !is_numeric($nl->_shortname)) { - $nl->save(); - } else { - $page->trigError("Le nom de la NL n'est pas valide."); - pl_redirect('admin/newsletter/edit/' . $nl->_id); + + // Save common fields + $issue->title = Post::s('title'); + $issue->title_mail = Post::s('title_mail'); + $issue->head = Post::s('head'); + $issue->signature = Post::s('signature'); + + if ($issue->isEditable()) { + // Date and shortname may only be modified for pending NLs, otherwise all links get broken. + $issue->date = Post::s('date'); + $issue->shortname = strlen(Post::blank('shortname')) ? null : Post::s('shortname'); + $issue->sufb->updateFromEnv($ufb->getEnv()); + + if ($nl->automaticMailingEnabled()) { + $issue->send_before = preg_replace('/^(\d\d\d\d)(\d\d)(\d\d)$/', '\1-\2-\3', Post::v('send_before_date')) . ' ' . Post::i('send_before_time_Hour') . ':00:00'; + } + } + $errors = $issue->save(); + if (count($errors)) { + foreach ($errors as $error_code) { + $page->trigError($error_msgs[$error_code]); + } } } + // Delete an article + if($action == 'delete') { + $issue->delArticle($aid); + pl_redirect($nl->adminPrefix() . "/edit/$nid"); + } + + // Save an article if(Post::v('save')) { $art = new NLArticle(Post::v('title'), Post::v('body'), Post::v('append'), $aid, Post::v('cid'), Post::v('pos')); - $nl->saveArticle($art); - pl_redirect("admin/newsletter/edit/$nid"); + $issue->saveArticle($art); + pl_redirect($nl->adminPrefix() . "/edit/$nid"); } + // Edit an article if ($action == 'edit' && $aid != 'update') { $eaid = $aid; if (Post::has('title')) { $art = new NLArticle(Post::v('title'), Post::v('body'), Post::v('append'), $eaid, Post::v('cid'), Post::v('pos')); } else { - $art = ($eaid == 'new') ? new NLArticle() : $nl->getArt($eaid); + $art = ($eaid == 'new') ? new NLArticle() : $issue->getArt($eaid); } if ($art && !$art->check()) { $page->trigError("Cet article est trop long."); @@ -156,12 +228,13 @@ class NewsletterModule extends PLModule $page->assign('art', $art); } + // Check blacklisted IPs if ($aid == 'blacklist_check') { global $globals; $ips_to_check = array(); $blacklist_host_resolution_count = 0; - foreach ($nl->_arts as $key => $articles) { + foreach ($issue->arts as $key => $articles) { foreach ($articles as $article) { $article_ips = $article->getLinkIps($blacklist_host_resolution_count); if (!empty($article_ips)) { @@ -179,14 +252,109 @@ class NewsletterModule extends PLModule } } + if ($issue->state == NLIssue::STATE_SENT) { + $page->trigWarning("Cette lettre a déjà été envoyée ; il est recommandé de limiter les modifications au maximum (orthographe, adresses web et mail)."); + } + + $ufb->setEnv($issue->sufb->getEnv()); $page->assign_by_ref('nl', $nl); + $page->assign_by_ref('issue', $issue); + } + + /** This handler will cancel the sending of the currently pending issue + * It is disabled for X.org mailings. + */ + function handler_admin_nl_cancel(&$page, $nid, $force = null) + { + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } + + if (!$nl->mayEdit() || !S::has_xsrf_token()) { + return PL_FORBIDDEN; + } + + if (!$nid) { + $page->kill("La lettre n'a pas été spécifiée"); + } + + $issue = $nl->getIssue($nid); + if (!$issue) { + $page->kill("La lettre {$nid} n'existe pas."); + } + if (!$issue->cancelMailing()) { + $page->trigErrorRedirect("Une erreur est survenue lors de l'annulation de l'envoi.", $nl->adminPrefix()); + } + + $page->trigSuccessRedirect("L'envoi de l'annonce {$issue->title()} est annulé.", $nl->adminPrefix()); + } + + /** This handler will enable the sending of the currently pending issue + * It is disabled for X.org mailings. + */ + function handler_admin_nl_valid(&$page, $nid, $force = null) + { + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } + + if (!$nl->mayEdit() || !S::has_xsrf_token()) { + return PL_FORBIDDEN; + } + + if (!$nid) { + $page->kill("La lettre n'a pas été spécifiée."); + } + + $issue = $nl->getIssue($nid); + if (!$issue) { + $page->kill("La lettre {$nid} n'existe pas."); + } + if (!$issue->scheduleMailing()) { + $page->trigErrorRedirect("Une erreur est survenue lors de la validation de l'envoi.", $nl->adminPrefix()); + } + + $page->trigSuccessRedirect("L'envoi de la newsletter {$issue->title()} a été validé.", $nl->adminPrefix()); + } + + /** This handler will remove the given issue. + */ + function handler_admin_nl_delete(&$page, $nid, $force = null) + { + $nl = $this->getNl(); + if (!$nl) { + return PL_NOT_FOUND; + } + + if (!$nl->mayEdit() || !S::has_xsrf_token()) { + return PL_FORBIDDEN; + } + + if (!$nid) { + $page->kill("La lettre n'a pas été spécifiée."); + } + + $issue = $nl->getIssue($nid); + if (!$issue) { + $page->kill("La lettre {$nid} n'existe pas"); + } + if (!$issue->isEditable()) { + $page->trigErrorRedirect("La lette a été envoyée ou est en cours d'envoi, elle ne peut être supprimée.", $nl->adminPrefix()); + } + if (!$issue->delete()) { + $page->trigErrorRedirect("Une erreur est survenue lors de la suppression de la lettre.", $nl->adminPrefix()); + } + + $page->trigSuccessRedirect("La lettre a bien été supprimée.", $nl->adminPrefix()); } function handler_admin_nl_cat(&$page, $action = 'list', $id = null) { $page->setTitle('Administration - Newsletter : Catégories'); $page->assign('title', 'Gestion des catégories de la newsletter'); $table_editor = new PLTableEditor('admin/newsletter/categories','newsletter_cat','cid'); - $table_editor->describe('titre','intitulé',true); + $table_editor->describe('title','intitulé',true); $table_editor->describe('pos','position',true); $table_editor->apply($page, $action, $id); } diff --git a/modules/xnetnl.php b/modules/xnetnl.php new file mode 100644 index 0000000..aa769be --- /dev/null +++ b/modules/xnetnl.php @@ -0,0 +1,48 @@ + $this->make_hook('nl', AUTH_MDP), + '%grp/nl/show' => $this->make_hook('nl_show', AUTH_MDP), + '%grp/admin/nl' => $this->make_hook('admin_nl', AUTH_MDP, 'groupadmin'), + '%grp/admin/nl/edit' => $this->make_hook('admin_nl_edit', AUTH_MDP, 'groupadmin'), + '%grp/admin/nl/edit/cancel' => $this->make_hook('admin_nl_cancel', AUTH_MDP, 'groupadmin'), + '%grp/admin/nl/edit/valid' => $this->make_hook('admin_nl_valid', AUTH_MDP, 'groupadmin'), + '%grp/admin/nl/categories' => $this->make_hook('admin_nl_cat', AUTH_MDP, 'groupadmin'), + ); + } + + protected function getNl() + { + global $globals; + $group = $globals->asso('shortname'); + return NewsLetter::forGroup($group); + } +} + +// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: +?> diff --git a/templates/admin/index.tpl b/templates/admin/index.tpl index 33246be..3764853 100644 --- a/templates/admin/index.tpl +++ b/templates/admin/index.tpl @@ -174,11 +174,11 @@ - Newsletter + Newsletters - Liste + Liste des NLs groupes   |   - Catégories + NL de X.org diff --git a/templates/axletter/admin.tpl b/templates/axletter/admin.tpl deleted file mode 100644 index 7f43373..0000000 --- a/templates/axletter/admin.tpl +++ /dev/null @@ -1,51 +0,0 @@ -{**************************************************************************} -{* *} -{* Copyright (C) 2003-2011 Polytechnique.org *} -{* http://opensource.polytechnique.org/ *} -{* *} -{* This program is free software; you can redistribute it and/or modify *} -{* it under the terms of the GNU General Public License as published by *} -{* the Free Software Foundation; either version 2 of the License, or *} -{* (at your option) any later version. *} -{* *} -{* This program is distributed in the hope that it will be useful, *} -{* but WITHOUT ANY WARRANTY; without even the implied warranty of *} -{* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *} -{* GNU General Public License for more details. *} -{* *} -{* You should have received a copy of the GNU General Public License *} -{* along with this program; if not, write to the Free Software *} -{* Foundation, Inc., *} -{* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *} -{* *} -{**************************************************************************} - -

Droits d'administration des lettres de l'AX

- -
- {xsrf_token_field} - - - - - - - - - {foreach item=a from=$admins} - - - - - {/foreach} -
NomAction
- - -
{profile user=$a promo=true}{icon name=cross title="Retirer"}
-
- -

Ajout d'utilisateurs

- -{include core=csv-importer.tpl} - -{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *} diff --git a/templates/axletter/edit.tpl b/templates/axletter/edit.tpl deleted file mode 100644 index 1bb758b..0000000 --- a/templates/axletter/edit.tpl +++ /dev/null @@ -1,121 +0,0 @@ -{**************************************************************************} -{* *} -{* Copyright (C) 2003-2011 Polytechnique.org *} -{* http://opensource.polytechnique.org/ *} -{* *} -{* This program is free software; you can redistribute it and/or modify *} -{* it under the terms of the GNU General Public License as published by *} -{* the Free Software Foundation; either version 2 of the License, or *} -{* (at your option) any later version. *} -{* *} -{* This program is distributed in the hope that it will be useful, *} -{* but WITHOUT ANY WARRANTY; without even the implied warranty of *} -{* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *} -{* GNU General Public License for more details. *} -{* *} -{* You should have received a copy of the GNU General Public License *} -{* along with this program; if not, write to the Free Software *} -{* Foundation, Inc., *} -{* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *} -{* *} -{**************************************************************************} - -

Édition de message

- -
- {xsrf_token_field} - {if $am} - {include file="axletter/letter.mail.tpl"} - -

- - - - {if $echeance} - - {/if} - {if !$new} - - {/if} -

- {/if} - -
- Sujet de l'email : -

- - {icon name=information title="Syntaxe wiki"} Voir les marqueurs de mise en forme autorisés -
- Titre :
-
- Signature : -

-
- - - - - - - - - - {include file="include/field.promo.tpl" prefix=""} - - - - - - - - - {if !$saved} - - - - - {else} - - - - {/if} -
Options du message
Nom raccourci - - (uniquement lettres, chiffres ou -) -
Envoyer à une liste d'adresses -
- Indiquez une liste d'adresses emails : la lettre sera envoyée uniquement aux personnes des promotions sélectionnées, dont l'adresse figure dans la liste, et qui souhaitent recevoir les emails de l'AX. -
Sélection inversée - En cochant cette case, la liste sera envoyée à tous les inscrits de l'intervalle de promotions sélectionné, sauf ceux indiqués dans la liste ci-dessus. -
Echéance d'envoi - le {valid_date name="echeance_date" value=$echeance_date from=3 to=15} - vers -
- Envoi au plus tard le {$echeance|date_format:"%x vers %Hh"}
- {if $is_xorg} - [{* - *}{icon name=thumb_up} Valider l'envoi] - {else} - [{* - *}{icon name=thumb_down} Annuler l'envoi] - {/if} -
- -

- - - - {if $echeance} - - {/if} - - {if $subset} - - {/if} - {if !$new} - - {/if} -

-
- -{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *} diff --git a/templates/axletter/index.tpl b/templates/axletter/index.tpl deleted file mode 100644 index 0260463..0000000 --- a/templates/axletter/index.tpl +++ /dev/null @@ -1,83 +0,0 @@ -{**************************************************************************} -{* *} -{* Copyright (C) 2003-2011 Polytechnique.org *} -{* http://opensource.polytechnique.org/ *} -{* *} -{* This program is free software; you can redistribute it and/or modify *} -{* it under the terms of the GNU General Public License as published by *} -{* the Free Software Foundation; either version 2 of the License, or *} -{* (at your option) any later version. *} -{* *} -{* This program is distributed in the hope that it will be useful, *} -{* but WITHOUT ANY WARRANTY; without even the implied warranty of *} -{* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *} -{* GNU General Public License for more details. *} -{* *} -{* You should have received a copy of the GNU General Public License *} -{* along with this program; if not, write to the Free Software *} -{* Foundation, Inc., *} -{* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *} -{* *} -{**************************************************************************} - - -

- Envoi exceptionnel de l'AX -

- -

Ton statut

- -{if $axs} -

-Tu es actuellement inscrit aux envois exceptionnels de l'AX (pour choisir le format HTML ou texte, rends toi sur la page des préférences). -

- -{else} -

-Tu n'es actuellement pas inscrit aux envois exceptionnels de l'AX. -

- -{/if} - -

Les archives

- - - - - - - {if $ax_rights && !$new} - - - - {elseif $ax_rights && $new} - - - - - {/if} - {foreach item=al from=$ax_list} - - - - - {/foreach} -
datetitre
- {icon name=page_edit} Proposer un nouvel email -
{icon name=page_edit} Éditer la demande - {if $new->title()} - {$new->title(true)} - {/if} -
{$al.date|date_format} - {$al.titre} -
- -{if $ax_rights} -

Il y a actuellement {$count} inscrits aux envois.

-{/if} - -{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *} diff --git a/templates/axletter/show.tpl b/templates/axletter/show.tpl deleted file mode 100644 index bb159fb..0000000 --- a/templates/axletter/show.tpl +++ /dev/null @@ -1,59 +0,0 @@ -{**************************************************************************} -{* *} -{* Copyright (C) 2003-2011 Polytechnique.org *} -{* http://opensource.polytechnique.org/ *} -{* *} -{* This program is free software; you can redistribute it and/or modify *} -{* it under the terms of the GNU General Public License as published by *} -{* the Free Software Foundation; either version 2 of the License, or *} -{* (at your option) any later version. *} -{* *} -{* This program is distributed in the hope that it will be useful, *} -{* but WITHOUT ANY WARRANTY; without even the implied warranty of *} -{* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *} -{* GNU General Public License for more details. *} -{* *} -{* You should have received a copy of the GNU General Public License *} -{* along with this program; if not, write to the Free Software *} -{* Foundation, Inc., *} -{* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *} -{* *} -{**************************************************************************} - -

- {if $am->_date} - Lettre de l'AX du {$am->_date|date_format} - {else} - Lettre de l'AX en préparation - {/if} -

- -

-{if $smarty.get.text} -[version HTML] -{else} -[version Texte] -{/if} -{if !$am->_date} -[éditer] -{/if} -

- -{include file="include/massmailer-nav.tpl" mm=$am base=ax} - -
-
- -
-
- - - - - - -
{$am->title(true)}
- {include file="axletter/letter.mail.tpl"} -
- -{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *} diff --git a/templates/axletter/unsubscribe.tpl b/templates/axletter/unsubscribe.tpl deleted file mode 100644 index 51e7042..0000000 --- a/templates/axletter/unsubscribe.tpl +++ /dev/null @@ -1,41 +0,0 @@ -{**************************************************************************} -{* *} -{* Copyright (C) 2003-2011 Polytechnique.org *} -{* http://opensource.polytechnique.org/ *} -{* *} -{* This program is free software; you can redistribute it and/or modify *} -{* it under the terms of the GNU General Public License as published by *} -{* the Free Software Foundation; either version 2 of the License, or *} -{* (at your option) any later version. *} -{* *} -{* This program is distributed in the hope that it will be useful, *} -{* but WITHOUT ANY WARRANTY; without even the implied warranty of *} -{* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *} -{* GNU General Public License for more details. *} -{* *} -{* You should have received a copy of the GNU General Public License *} -{* along with this program; if not, write to the Free Software *} -{* Foundation, Inc., *} -{* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *} -{* *} -{**************************************************************************} - -

Désinscription des envois de l'AX

- -{if $success} -

- Votre désinscription aux envois exceptionnels de l'AX a été réalisée avec - succès. Si vous désirez vous réinscrire, merci de contacter - l'AX. Vous pouvez également - le faire en vous inscrivant à Polytechnique.org (pour - les X uniquement). -

-{else} -

- Votre inscription aux envois de l'AX n'a pu être résiliée. Merci de contacter - au plus vite l'AX pour faire - part de ce problème. -

-{/if} - -{* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *} diff --git a/templates/include/form.valid.edit-nl.tpl b/templates/include/form.valid.edit-nl.tpl index f2b442e..35abde7 100644 --- a/templates/include/form.valid.edit-nl.tpl +++ b/templates/include/form.valid.edit-nl.tpl @@ -20,11 +20,11 @@ {* *} {**************************************************************************} - +
- +
- + {* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *} diff --git a/templates/include/massmailer-nav.tpl b/templates/include/massmailer-nav.tpl index b634f6e..3c5a462 100644 --- a/templates/include/massmailer-nav.tpl +++ b/templates/include/massmailer-nav.tpl @@ -22,28 +22,28 @@ - {if $mm->prev() neq null} + {if $issue->prev() neq null} {/if} - {if $mm->next() neq null} + {if $issue->next() neq null} {/if} - {if $mm->last() neq null} + {if $issue->last() neq null} diff --git a/templates/newsletter/admin.tpl b/templates/newsletter/admin.tpl index 522403e..e4d1782 100644 --- a/templates/newsletter/admin.tpl +++ b/templates/newsletter/admin.tpl @@ -22,22 +22,24 @@

- Lettre de Polytechnique.org + {$nl->name}

- + {icon name=resultset_previous title="Lettre précédente"}Lettre précédente - [Liste des lettres] + [Liste des lettres] - + Lettre suivante{icon name=resultset_next title="Lettre suivante"}
- + Dernière lettre
- - + + + - + - {foreach item=nl from=$nl_list} + {foreach item=nli from=$nl_list} - + + {/foreach} diff --git a/templates/newsletter/edit.tpl b/templates/newsletter/edit.tpl index 6912d5b..f470439 100644 --- a/templates/newsletter/edit.tpl +++ b/templates/newsletter/edit.tpl @@ -21,17 +21,18 @@ {**************************************************************************}

- Lettre de Polytechnique.org de {$nl->_date|date_format:"%B %Y"} + {$nl->name} de {$issue->date|date_format:"%B %Y"}

{if !$art}

-[liste] -[visualiser] +[liste] +[visualiser] +

- +
datetitreDateÉtatTitre
Créer une nouvelle lettreCréer une nouvelle lettre
{$nl.date|date_format}{$nli->date|date_format}{$nli->state} - {$nl.titre|default:"[no title]"} + {$nli->title()|default:"[Sans titre]"}
+ + @@ -51,8 +77,12 @@ Nom @@ -60,7 +90,7 @@ Titre de l'email @@ -68,28 +98,100 @@ Titre + + + + + {if $nl->automaticMailingEnabled() && ($issue->isEditable() || $issue->isPending())} + + + + + {/if} + {if $issue->isEditable()} + {if $nl->criteria->hasFlag('promo')} + + + + + {/if} + {if $nl->criteria->hasFlag('axid')} + + + + + {/if} + {/if}
@@ -40,10 +41,35 @@
+ État + +{if $issue->isPending()} + En attente d'envoi + {if $nl->automaticMailingEnabled()} + [{* + *}{icon name=delete} Annuler l'envoi] + {/if} +{elseif $issue->isEditable()} + En cours d'édition + + {if $nl->automaticMailingEnabled()} + [{* + *}{icon name=tick} Valider l'envoi] + {/if} + + [{* + *}{icon name=cross} Supprimer] +{else} + Envoyée +{/if} + +
ID - {$nl->_id} + {$issue->id}
- - (Ex : 2006-06 pour la NL de juin 2006) + {if $issue->isEditable()} + + (Ex : 2006-06 pour la NL de juin 2006) + {else} + {$issue->shortname} + {/if}
- +
- +
- Date d'envoi + Date + + {if $issue->isEditable()} + {valid_date name="date" value=$issue->date from=0 to=60} + {else} + {$issue->date} + {/if} +
+ Intro de la lettre
(ou contenu pour les lettres exceptionnelles)
- +
- Intro de la lettre + Signature de la lettre - +
+ Date d'envoi + + {if $issue->isEditable()} + Le {valid_date name="send_before_date" value=$issue->getSendBeforeDate() from=3 to=15} vers {html_select_time prefix="send_before_time_" time=$issue->getSendBeforeTime() display_hours=true display_minutes=false display_seconds=false display_meridian=false use_24_hours=true} heures + {else} + Le {$issue->send_before|date_format:"%X vers %Hh"} + {/if} +
+ Promotions + + + + +  et  + + +
Matricule AX + +
+ Entrer une liste de matricules AX (un par ligne)
+ +
- +
@@ -103,13 +205,13 @@ Créer un nouvel article… - {icon name=add title="créer"} + {icon name=add title="créer"} - {foreach from=$nl->_arts item=arts key=cat} + {foreach from=$issue->arts item=arts key=cat} - {$nl->_cats[$cat]|default:"[no cat]"} + {$issue->category($cat)|default:"[no category]"} @@ -119,12 +221,12 @@
{$art->toText('%hash%','%login%')}
- Pos: {$art->_pos}
- + Pos: {$art->pos}
+
{icon name="page_edit" title="Editer"}


- {icon name="delete" title="Supprimer"} @@ -136,7 +238,7 @@
- +
@@ -166,7 +268,7 @@ {else}

-[retour] +[retour]

@@ -186,7 +288,7 @@
- +
@@ -213,7 +315,7 @@ diff --git a/templates/newsletter/index.tpl b/templates/newsletter/index.tpl index bda1f5f..4fd362c 100644 --- a/templates/newsletter/index.tpl +++ b/templates/newsletter/index.tpl @@ -22,29 +22,33 @@

- Lettre de Polytechnique.org + {$nl->name} +{if $nl->mayEdit() && $nl->adminLinksEnabled()} + [Administrer] +{/if}

- +{if $nl->maySubmit()}

- {icon name=page_edit value="Proposer un article"} Proposer un article pour la lettre mensuelle + {icon name=page_edit value="Proposer un article"} Proposer un article pour la {$nl->name}

+{/if}

Ton statut

-{if $nls} +{if $nl->subscriptionState()}

-Tu es actuellement inscrit à la lettre mensuelle de Polytechnique.org (pour choisir le format HTML ou texte, rends toi sur la page des préférences). +Tu es actuellement inscrit à la {$nl->name} (pour choisir le format HTML ou texte, rends toi sur la page des préférences).

{else}

-Tu n'es actuellement pas inscrit à la lettre mensuelle de Polytechnique.org. +Tu n'es actuellement pas inscrit à la {$nl->name}.

{/if} @@ -55,15 +59,19 @@ Tu n'es actuellement pas inscrit à la lettre mensuelle de Polytechnique.org. - {foreach item=nl from=$nl_list} + {foreach item=nli from=$nl_list} - + {/foreach}
@@ -204,8 +306,8 @@
Position - +
date titre
{$nl.date|date_format}{$nli.date|date_format} - {$nl.titre} + {$nli.title|default:"[Sans titre]"}
+{if $nl->mayEdit()} +

Il y a actuellement {$nl->subscriberCount()} inscrits aux envois.

+{/if} + {* vim:set et sw=2 sts=2 sws=2 enc=utf-8: *} diff --git a/templates/axletter/letter.mail.tpl b/templates/newsletter/nl.AX.mail.tpl similarity index 89% rename from templates/axletter/letter.mail.tpl rename to templates/newsletter/nl.AX.mail.tpl index ff64695..5a8b0b8 100644 --- a/templates/axletter/letter.mail.tpl +++ b/templates/newsletter/nl.AX.mail.tpl @@ -23,7 +23,7 @@ {config_load file="mails.conf" section="mails_ax"} {if $mail_part eq 'head'} {from full=#from#} -{subject text=$am->title(true)} +{subject text=$issue->title(true)} {if isset(#replyto#)}{add_header name='Reply-To' value=#replyto#}{/if} {if isset(#retpath#)}{add_header name='Return-Path' value=#retpath#}{/if} {elseif $mail_part eq 'text'} @@ -31,14 +31,12 @@
 {/if}
 ====================================================================
-{$am->title()}
+{$issue->title()}
 ====================================================================
 
-{$am->head($user, 'text')}
+{$issue->head($user, 'text')}
 
-{$am->body('text')}
-
-{$am->signature('text')}
+{$issue->signature('text')}
 
 --------------------------------------------------------------------
 Cette lettre est envoyée par l'AX grâce aux outils de Polytechnique.org.
@@ -66,7 +64,7 @@ ne plus recevoir : <https://www.polytechnique.org/ax/out{if $hash}/{$hash}{/i
       body      { background-color: #ddd; color: #000; }
       {/literal}
     
     
   
@@ -74,10 +72,9 @@ ne plus recevoir : <https://www.polytechnique.org/ax/out{if $hash}/{$hash}{/i
     
{/if}
-
{$am->title()}
-
{$am->head($user, 'html')|smarty:nodefaults}
-
{$am->body('html')|smarty:nodefaults}
-
{$am->signature('html')|smarty:nodefaults}
+
{$issue->title()}
+
{$issue->head($user, 'html')|smarty:nodefaults}
+
{$issue->signature('html')|smarty:nodefaults}
Cette lettre est envoyée par l'AX grâce aux outils de Polytechnique.org.
diff --git a/templates/newsletter/nl.mail.tpl b/templates/newsletter/nl.Polytechnique.org.mail.tpl similarity index 85% rename from templates/newsletter/nl.mail.tpl rename to templates/newsletter/nl.Polytechnique.org.mail.tpl index 1d1ea52..43ddf9f 100644 --- a/templates/newsletter/nl.mail.tpl +++ b/templates/newsletter/nl.Polytechnique.org.mail.tpl @@ -23,7 +23,7 @@ {config_load file="mails.conf" section="newsletter"} {if $mail_part eq 'head'} {from full=#from#} -{subject text=$nl->title(true)} +{subject text=$issue->title(true)} {if isset(#replyto#)}{add_header name='Reply-To' value=#replyto#}{/if} {if isset(#retpath#)}{add_header name='Return-Path' value=#retpath#}{/if} {elseif $mail_part eq 'text'} @@ -31,23 +31,23 @@
 {/if}
 ====================================================================
-{$nl->title()}
+{$issue->title()}
 ====================================================================
 
-{$nl->head($user, 'text')}
+{$issue->head($user, 'text')}
 
 
-{foreach from=$nl->_arts key=cid item=arts name=cats}
-{$smarty.foreach.cats.iteration} *{$nl->_cats[$cid]}*
+{foreach from=$issue->arts key=cid item=arts name=cats}
+{$smarty.foreach.cats.iteration} *{$issue->category($cid)}*
 {foreach from=$arts item=art}
 - {$art->title()}
 {/foreach}
 
 {/foreach}
 
-{foreach from=$nl->_arts key=cid item=arts}
+{foreach from=$issue->arts key=cid item=arts}
 --------------------------------------------------------------------
-*{$nl->_cats[$cid]}*
+*{$issue->category($cid)}*
 --------------------------------------------------------------------
 
 {foreach from=$arts item=art}
@@ -84,7 +84,7 @@ ne plus recevoir : <https://www.polytechnique.org/nl/out>
       body      { background-color: #ddd; color: #000; }
       {/literal}
     
     
   
@@ -92,21 +92,21 @@ ne plus recevoir : <https://www.polytechnique.org/nl/out>
     
{/if}
- -
{$nl->head($user, 'html')|smarty:nodefaults}
+ +
{$issue->head($user, 'html')|smarty:nodefaults}
- {foreach from=$nl->_arts key=cid item=arts name=cats} + {foreach from=$issue->arts key=cid item=arts name=cats} {/foreach} - {foreach from=$nl->_arts key=cid item=arts name=cats} + {foreach from=$issue->arts key=cid item=arts name=cats}

- {$nl->_cats[$cid]} + {$issue->category($cid)}

{foreach from=$arts item=art} {$art->toHtml($hash, $user->login())|smarty:nodefaults} diff --git a/templates/newsletter/show.tpl b/templates/newsletter/show.tpl index b861e52..39f83a6 100644 --- a/templates/newsletter/show.tpl +++ b/templates/newsletter/show.tpl @@ -21,21 +21,21 @@ {**************************************************************************}

- Lettre de Polytechnique.org du {$nl->_date|date_format} + {$issue->nl->name} du {$issue->date|date_format}

{if $smarty.get.text} - [version HTML] + [version HTML] {else} - [version Texte] + [version Texte] {/if} - {if hasPerm('admin')} - [Éditer] + {if $nl->mayEdit()} + [Éditer] {/if}

-{include file="include/massmailer-nav.tpl" mm=$nl base=nl} +{include file="include/massmailer-nav.tpl" issue=$issue nl=$nl}
@@ -44,10 +44,10 @@ - +
{$nl->title(true)}
{$issue->title(true)}
- {include file="newsletter/nl.mail.tpl" escape=true} + {include file=$nl->tplFile() escape=true}
diff --git a/upgrade/1.1.0/20_newsletter.sql b/upgrade/1.1.0/20_newsletter.sql new file mode 100644 index 0000000..24ced53 --- /dev/null +++ b/upgrade/1.1.0/20_newsletter.sql @@ -0,0 +1,171 @@ +DROP TABLE IF EXISTS newsletter_issues; +DROP TABLE IF EXISTS newsletters; + +----------------- +-- newsletters -- +----------------- + +CREATE TABLE newsletters ( + id int(11) unsigned NOT NULL auto_increment, + group_id smallint(5) UNSIGNED NOT NULL, + name varchar(255) NOT NULL, + custom_css BOOL NOT NULL DEFAULT FALSE, + criteria SET('axid', 'promo', 'geo') DEFAUL NULL, + PRIMARY KEY (id), + UNIQUE KEY (group_id), + FOREIGN KEY (group_id) REFERENCES groups (id) + ON UPDATE CASCADE + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Lists all newsletters'; + +-- Filling it with default values for X.org / AX / Ecole +INSERT INTO newsletters (group_id, name, custom_css) + ( SELECT groups.id, CONCAT("Lettre de ", groups.nom), TRUE + FROM groups + WHERE groups.diminutif IN ('Polytechnique.org', 'Ecole', 'AX') + ); + +-- Set variables for simpler queries later on +SET @idnl_xorg = (SELECT nls.id FROM newsletters AS nls LEFT JOIN groups AS g ON (g.id = nls.group_id) WHERE g.diminutif = 'Polytechnique.org'); +SET @idnl_ax = (SELECT nls.id FROM newsletters AS nls LEFT JOIN groups AS g ON (g.id = nls.group_id) WHERE g.diminutif = 'AX'); +SET @idnl_ecole = (SELECT nls.id FROM newsletters AS nls LEFT JOIN groups AS g ON (g.id = nls.group_id) WHERE g.diminutif = 'Ecole'); + +UPDATE newsletters SET name = "Lettre de l'AX", criteria = 'promo,axid' WHERE id = @idnl_ax; +UPDATE newsletters SET name = "Lettre mensuelle de Polytechnique.org", criteria = 'promo' WHERE id = @idnl_xorg; +UPDATE newsletters SET name = "DiXit, lettre de l'École polytechnique", criteria = 'promo' WHERE id = @idnl_ecole; + +----------------------- +-- newsletter_issues -- +----------------------- + +CREATE TABLE newsletter_issues ( + nlid int(11) unsigned NOT NULL, + id int(11) unsigned NOT NULL auto_increment, + date date NOT NULL default '0000-00-00', + send_before date default NULL, + state enum('sent','new','pending') NOT NULL default 'new', + sufb_json text default NULL, + title varchar(255) NOT NULL default '', + head mediumtext NOT NULL, + signature mediumtext NOT NULL, + short_name varchar(16) default NULL, + mail_title varchar(255) NOT NULL default '', + PRIMARY KEY (id), + UNIQUE KEY (nlid, short_name), + FOREIGN KEY (nlid) REFERENCES newsletters (id) + ON UPDATE CASCADE + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Lists issues of all newsletters'; + +-- Fill with all X.org nls +INSERT INTO newsletter_issues (nlid, date, title, state, head, short_name, mail_title) + ( + SELECT @idnl_xorg, n.date, n.titre, n.bits, n.head, n.short_name, n.titre_mail + FROM newsletter AS n + ); + +-- Fill with all AX nls +INSERT INTO newsletter_issues (nlid, date, title, state, head, signature, short_name, mail_title) + ( + SELECT @idnl_ax, a.date, a.title, a.bits, CONCAT(" ,\n\n", a.body), a.signature, a.short_name, a.subject + FROM axletter AS a + WHERE bits != 'invalid' + ); + +-------------------- +-- newsletter_cat -- +-------------------- + +-- Fix newsletter_cat: add nlid, add FK, rename title +ALTER TABLE newsletter_cat ADD COLUMN nlid INT(11) UNSIGNED NOT NULL AFTER cid; + +UPDATE newsletter_cat SET nlid = @idnl_xorg; + +ALTER TABLE newsletter_cat ADD FOREIGN KEY (nlid) REFERENCES newsletters (id) + ON UPDATE CASCADE + ON DELETE CASCADE; +ALTER TABLE newsletter_cat CHANGE titre title varchar(128) NOT NULL DEFAULT ''; + +-- Final state: +-- +-- CREATE TABLE `newsletter_cat` ( +-- `cid` tinyint(3) unsigned NOT NULL auto_increment, +-- `nlid` int(11) unsigned NOT NULL, +-- `pos` tinyint(3) unsigned NOT NULL default '0', +-- `title` varchar(128) NOT NULL default '', +-- PRIMARY KEY (`cid`), +-- KEY `pos` (`pos`), +-- KEY `nlid` (`nlid`), +-- CONSTRAINT `newsletter_cat_ibfk_1` FOREIGN KEY (`nlid`) REFERENCES `newsletters` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +-- ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 + + +-------------------- +-- newsletter_art -- +-------------------- + +-- Fix newsletter_cat: add nlid, add FK, rename title +ALTER TABLE newsletter_art DROP FOREIGN KEY newsletter_art_ibfk_1; + UPDATE newsletter_art AS na +LEFT JOIN newsletter AS n ON na.id = n.id +LEFT JOIN newsletter_issues AS ni ON (ni.nlid = @idnl_xorg AND ni.short_name = n.short_name) + SET na.id = ni.id; + +ALTER TABLE newsletter_art ADD FOREIGN KEY (id) REFERENCES newsletter_issues (id) + ON UPDATE CASCADE + ON DELETE CASCADE + +-------------------- +-- newsletter_ins -- +-------------------- + +-- Fix newsletter_ins: add nlid column, fix FK +ALTER TABLE newsletter_ins ADD COLUMN nlid INT(11) UNSIGNED NOT NULL AFTER uid; + +UPDATE newsletter_ins SET nlid = @idnl_xorg; + +-- We have to drop all FKs in order to update 'last' indexes. +ALTER TABLE newsletter_ins DROP FOREIGN KEY newsletter_ins_ibfk_1; +ALTER TABLE newsletter_ins DROP FOREIGN KEY newsletter_ins_ibfk_2; +ALTER TABLE newsletter_ins DROP PRIMARY KEY; + + UPDATE newsletter_ins AS ni +LEFT JOIN newsletter AS n ON (ni.last = n.id) +LEFT JOIN newsletter_issues AS ns ON (n.short_name = ns.short_name) + SET ni.last = ns.id; + +ALTER TABLE newsletter_ins ADD PRIMARY KEY (uid, nlid); +ALTER TABLE newsletter_ins ADD FOREIGN KEY (uid) REFERENCES accounts (uid) + ON UPDATE CASCADE + ON DELETE CASCADE; +ALTER TABLE newsletter_ins ADD FOREIGN KEY (last) REFERENCES newsletter_issues (id) + ON UPDATE CASCADE + ON DELETE CASCADE; +ALTER TABLE newsletter_ins ADD FOREIGN KEY (nlid) REFERENCES newsletters (id) + ON UPDATE CASCADE + ON DELETE CASCADE; + +-- Add AXletter subscribers. +INSERT INTO newsletter_ins (nlid, uid, last, hash) + ( + SELECT @idnl_ax, ai.uid, MAX(ni.id), ai.hash + FROM axletter_ins AS ai + LEFT JOIN axletter AS a ON (ai.last = a.id) + LEFT JOIN newsletter_issues AS ni ON (ni.nlid = @idnl_ax AND ni.short_name = a.short_name) + GROUP BY ai.uid + ); + +-- Final state: +-- +-- CREATE TABLE `newsletter_ins` ( +-- `uid` int(11) unsigned NOT NULL default '0', +-- `nlid` int(11) unsigned NOT NULL, +-- `last` int(11) unsigned default NULL, +-- `hash` varchar(32) default NULL, +-- PRIMARY KEY (`uid`,`nlid`), +-- KEY `last` (`last`), +-- KEY `nlid` (`nlid`), +-- CONSTRAINT `newsletter_ins_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `accounts` (`uid`) ON DELETE CASCADE ON UPDATE CASCADE, +-- CONSTRAINT `newsletter_ins_ibfk_2` FOREIGN KEY (`last`) REFERENCES `newsletter_issues` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, +-- CONSTRAINT `newsletter_ins_ibfk_3` FOREIGN KEY (`nlid`) REFERENCES `newsletters` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='liste des abonnés à la newsletter' -- 2.1.4