diff --git a/config/services.yml b/config/services.yml index b0343ebc5..7312bc6e4 100644 --- a/config/services.yml +++ b/config/services.yml @@ -334,6 +334,7 @@ services: - '@phpbb.titania.message' - '@phpbb.titania.access' - '@phpbb.titania.subscriptions' + - '@path_helper' - '@phpbb.titania.attachment.operator' phpbb.titania.queue.stats: @@ -431,18 +432,61 @@ services: tags: - { name: titania.contribution.type } + phpbb.titania.notification.type.base: + abstract: true + class: phpbb\titania\notification\type\base + parent: notification.type.base + calls: + - [set_user_loader, ['@user_loader']] + + phpbb.titania.notification.type.posted: + class: phpbb\titania\notification\type\posted + shared: false + parent: phpbb.titania.notification.type.base + tags: + - { name: notification.type } + + phpbb.titania.notification.type.contribution: + class: phpbb\titania\notification\type\contribution + shared: false + parent: phpbb.titania.notification.type.base + tags: + - { name: notification.type } + + phpbb.titania.notification.type.queue: + class: phpbb\titania\notification\type\queue + shared: false + parent: phpbb.titania.notification.type.base + calls: + - [set_type_collection, ['@phpbb.titania.contribution.type.collection']] + tags: + - { name: notification.type } + + phpbb.titania.notification.type.queue_move: + class: phpbb\titania\notification\type\queue_move + shared: false + parent: phpbb.titania.notification.type.queue + tags: + - { name: notification.type } + + phpbb.titania.notification.type.attention: + class: phpbb\titania\notification\type\attention + shared: false + parent: phpbb.titania.notification.type.base + calls: + - [set_type_collection, ['@phpbb.titania.contribution.type.collection']] + tags: + - { name: notification.type } + phpbb.titania.subscriptions: class: phpbb\titania\subscriptions arguments: - '@dbal.conn' - - '@config' - '@request' - '@template' - '@user' - '@path_helper' - - '%tables.users%' - - '%core.root_path%' - - '%core.php_ext%' + - '@notification_manager' phpbb.titania.sync: class: phpbb\titania\sync diff --git a/controller/contribution/revision.php b/controller/contribution/revision.php index 72a8bb7ea..ce400d181 100644 --- a/controller/contribution/revision.php +++ b/controller/contribution/revision.php @@ -270,13 +270,18 @@ public function add($contrib_type, $contrib) 'U_VIEW' => $this->queue->get_url(), ); - $this->subscriptions->send_notifications( - ext::TITANIA_QUEUE, - $this->contrib->contrib_type, - 'subscribe_notify_forum', - $email_vars, - $this->user->data['user_id'] - ); + $this->subscriptions->send_notifications('queue', array( + 'item_id' => $this->queue->queue_id, + 'item_parent_id' => $this->contrib->contrib_id, + 'watch' => array(array(ext::TITANIA_QUEUE, $this->contrib->contrib_type)), + 'exclude_user' => $this->user->data['user_id'], + 'lang_key' => 'NOTIFICATION_TITANIA_QUEUE_NEW', + 'reference' => $this->contrib->contrib_name . ' ' . $this->revision->revision_version, + 'url' => $email_vars['U_VIEW'], + 'email_template' => 'subscribe_notify_forum', + 'email_vars' => $email_vars, + 'actor_id' => $this->user->data['user_id'], + )); } redirect($this->contrib->get_url()); } diff --git a/event/main_listener.php b/event/main_listener.php index 11227cac2..7a67b0bec 100644 --- a/event/main_listener.php +++ b/event/main_listener.php @@ -103,6 +103,8 @@ static public function getSubscribedEvents() { return array( 'core.permissions' => 'add_permissions', + 'core.user_setup' => 'load_language', + 'core.user_add_modify_notifications_data' => 'add_notifications_data', 'kernel.request' => array(array('startup', -1)), 'core.page_header_after' => 'overwrite_template_vars', 'core.text_formatter_s9e_configure_after' => 'inject_bbcode_code_lang', @@ -192,6 +194,50 @@ public function add_permissions($event) )); } + /** + * Load the notification language file on every page, so the notification + * dropdown and the UCP notification options can render Titania's types. + * + * @param data $event + */ + public function load_language($event) + { + $lang_set_ext = $event['lang_set_ext']; + $lang_set_ext[] = array( + 'ext_name' => 'phpbb/titania', + 'lang_set' => 'notifications', + ); + $event['lang_set_ext'] = $lang_set_ext; + } + + /** + * Give new users email delivery for the Titania notification types by + * default, like the core does for the post and topic types, so + * subscribing keeps its historical mail-me meaning. + * + * @param data $event + */ + public function add_notifications_data($event) + { + $notifications_data = $event['notifications_data']; + + foreach (ext::get_notification_types() as $type) + { + // queue_move shares the queue type's preferences + if ($type === 'phpbb.titania.notification.type.queue_move') + { + continue; + } + + $notifications_data[] = array( + 'item_type' => $type, + 'method' => 'notification.method.email', + ); + } + + $event['notifications_data'] = $notifications_data; + } + public function startup($event) { if ($event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST || diff --git a/ext.php b/ext.php index 389805fe8..aef4f9296 100644 --- a/ext.php +++ b/ext.php @@ -105,4 +105,80 @@ public static function get_filtered_repository_branches(): array { return [40]; } + + /** + * The notification types this extension provides + * + * @return array Array of notification type service names + */ + public static function get_notification_types(): array + { + return [ + 'phpbb.titania.notification.type.posted', + 'phpbb.titania.notification.type.contribution', + 'phpbb.titania.notification.type.queue', + 'phpbb.titania.notification.type.queue_move', + 'phpbb.titania.notification.type.attention', + ]; + } + + /** + * {@inheritdoc} + */ + public function enable_step($old_state) + { + if ($old_state === false) + { + $notification_manager = $this->container->get('notification_manager'); + + foreach (self::get_notification_types() as $type) + { + $notification_manager->enable_notifications($type); + } + + return 'notifications'; + } + + return parent::enable_step($old_state); + } + + /** + * {@inheritdoc} + */ + public function disable_step($old_state) + { + if ($old_state === false) + { + $notification_manager = $this->container->get('notification_manager'); + + foreach (self::get_notification_types() as $type) + { + $notification_manager->disable_notifications($type); + } + + return 'notifications'; + } + + return parent::disable_step($old_state); + } + + /** + * {@inheritdoc} + */ + public function purge_step($old_state) + { + if ($old_state === false) + { + $notification_manager = $this->container->get('notification_manager'); + + foreach (self::get_notification_types() as $type) + { + $notification_manager->purge_notifications($type); + } + + return 'notifications'; + } + + return parent::purge_step($old_state); + } } diff --git a/includes/objects/attention.php b/includes/objects/attention.php index a7398e087..67c0f104a 100644 --- a/includes/objects/attention.php +++ b/includes/objects/attention.php @@ -91,8 +91,12 @@ public function check_auth() public function submit() { - // Subscriptions - if (!$this->attention_id) + $is_new = !$this->attention_id; + + parent::submit(); + + // Subscriptions; sent after the parent submit so the attention id exists + if ($is_new) { $u_view = $this->controller_helper->route('phpbb.titania.manage.attention.redirect', array( 'type' => $this->attention_type, @@ -102,16 +106,35 @@ public function submit() 'NAME' => $this->attention_title, 'U_VIEW' => $this->path_helper->strip_url_params($u_view, 'sid'), ); - $this->subscriptions->send_notifications( - ext::TITANIA_ATTENTION, - 0, - 'subscribe_notify', - $email_vars, - $this->attention_poster_id - ); - } - parent::submit(); + switch ($this->attention_type) + { + case ext::TITANIA_ATTENTION_REPORTED: + $lang_key = 'NOTIFICATION_TITANIA_ATTENTION_REPORT'; + break; + + case ext::TITANIA_ATTENTION_UNAPPROVED: + $lang_key = 'NOTIFICATION_TITANIA_ATTENTION_UNAPPROVED'; + break; + + default: + $lang_key = 'NOTIFICATION_TITANIA_ATTENTION'; + break; + } + + $this->subscriptions->send_notifications('attention', array( + 'item_id' => $this->attention_id, + 'item_parent_id' => $this->attention_object_id, + 'watch' => array(array(ext::TITANIA_ATTENTION, 0)), + 'exclude_user' => $this->attention_poster_id, + 'lang_key' => $lang_key, + 'reference' => $this->attention_title, + 'url' => $email_vars['U_VIEW'], + 'email_template' => 'subscribe_notify', + 'email_vars' => $email_vars, + 'actor_id' => $this->attention_requester, + )); + } } /** diff --git a/includes/objects/attention_types/post.php b/includes/objects/attention_types/post.php index 8cfe2a54d..de8b8d31b 100644 --- a/includes/objects/attention_types/post.php +++ b/includes/objects/attention_types/post.php @@ -273,10 +273,12 @@ protected function approve_post() $message_vars = array( 'U_VIEW' => $this->path_helper->strip_url_params($u_view, 'sid'), ); - $object_type = array(ext::TITANIA_TOPIC, ext::TITANIA_SUPPORT); - $object_id = array($this->post->topic_id, $this->post->topic->parent_id); + $watch = array( + array(ext::TITANIA_TOPIC, $this->post->topic_id), + array(ext::TITANIA_SUPPORT, $this->post->topic->parent_id), + ); - $this->send_notifications($object_type, $object_id, 'subscribe_notify_contrib', $message_vars); + $this->send_notifications($watch, 'NOTIFICATION_TITANIA_REPLY_CONTRIB', 'subscribe_notify_contrib', $message_vars); } } @@ -296,20 +298,21 @@ protected function approve_topic() if ($this->post->topic->topic_last_post_id == $this->post->post_id) { $message_vars = array('U_VIEW' => $this->post->topic->get_url()); + $watch = array(array($this->post->post_type, $this->post->topic->parent_id)); - $this->send_notifications($this->post->post_type, $this->post->topic->parent_id, 'subscribe_notify_forum_contrib', $message_vars); + $this->send_notifications($watch, 'NOTIFICATION_TITANIA_TOPIC_CONTRIB', 'subscribe_notify_forum_contrib', $message_vars); } } /** * Send notifications. * - * @param int|array $object_type - * @param int|array $object_id + * @param array $watch Array of array(watch_object_type, watch_object_id) pairs + * @param string $lang_key Language key for the board notification title * @param string $email_template * @param array $message_vars */ - public function send_notifications($object_type, $object_id, $email_template, $message_vars) + public function send_notifications($watch, $lang_key, $email_template, $message_vars) { $this->load_contrib_object(); @@ -318,13 +321,19 @@ public function send_notifications($object_type, $object_id, $email_template, $m 'CONTRIB_NAME' => $this->contrib->contrib_name, )); - $this->subscriptions->send_notifications( - $object_type, - $object_id, - $email_template, - $message_vars, - $this->post->post_user_id - ); + $this->subscriptions->send_notifications('posted', array( + 'item_id' => $this->post->post_id, + 'item_parent_id' => $this->post->topic_id, + 'watch' => $watch, + 'exclude_user' => $this->post->post_user_id, + 'lang_key' => $lang_key, + 'lang_params' => array($this->contrib->contrib_name), + 'reference' => $this->post->topic->topic_subject, + 'url' => $message_vars['U_VIEW'], + 'email_template' => $email_template, + 'email_vars' => $message_vars, + 'actor_id' => $this->post->post_user_id, + )); } /** diff --git a/includes/objects/contribution.php b/includes/objects/contribution.php index c4b74f3d0..651a74f01 100644 --- a/includes/objects/contribution.php +++ b/includes/objects/contribution.php @@ -2313,12 +2313,28 @@ public function delete() // Change the status to new (handles resetting counts) $this->change_status(ext::TITANIA_CONTRIB_NEW); - // Remove any attention items + // Remove any attention items and their notifications + $attention_ids = array(); + $sql = 'SELECT attention_id FROM ' . TITANIA_ATTENTION_TABLE . ' + WHERE attention_object_type = ' . ext::TITANIA_CONTRIB . ' + AND attention_object_id = ' . $this->contrib_id; + $result = phpbb::$db->sql_query($sql); + while ($row = phpbb::$db->sql_fetchrow($result)) + { + $attention_ids[] = (int) $row['attention_id']; + } + phpbb::$db->sql_freeresult($result); + $sql = 'DELETE FROM ' . TITANIA_ATTENTION_TABLE . ' WHERE attention_object_type = ' . ext::TITANIA_CONTRIB . ' AND attention_object_id = ' . $this->contrib_id; phpbb::$db->sql_query($sql); + if (!empty($attention_ids)) + { + phpbb::$container->get('notification_manager')->delete_notifications('phpbb.titania.notification.type.attention', $attention_ids); + } + // Delete the release topic if ($this->contrib_release_topic_id) { diff --git a/includes/objects/post.php b/includes/objects/post.php index 15a0b1742..1ef265450 100644 --- a/includes/objects/post.php +++ b/includes/objects/post.php @@ -765,12 +765,32 @@ public function hard_delete() // @todo remove attachments and other things - // Remove any attention items + // Remove any attention items and their notifications + $attention_ids = array(); + $sql = 'SELECT attention_id FROM ' . TITANIA_ATTENTION_TABLE . ' + WHERE attention_object_type = ' . ext::TITANIA_POST . ' + AND attention_object_id = ' . $this->post_id; + $result = phpbb::$db->sql_query($sql); + while ($row = phpbb::$db->sql_fetchrow($result)) + { + $attention_ids[] = (int) $row['attention_id']; + } + phpbb::$db->sql_freeresult($result); + $sql = 'DELETE FROM ' . TITANIA_ATTENTION_TABLE . ' WHERE attention_object_type = ' . ext::TITANIA_POST . ' AND attention_object_id = ' . $this->post_id; phpbb::$db->sql_query($sql); + // Remove any notifications for this post and its attention items + $notification_manager = phpbb::$container->get('notification_manager'); + $notification_manager->delete_notifications('phpbb.titania.notification.type.posted', $this->post_id); + + if (!empty($attention_ids)) + { + $notification_manager->delete_notifications('phpbb.titania.notification.type.attention', $attention_ids); + } + // Decrement the user's postcount if we must if (!$this->post_deleted && $this->post_approved && in_array($this->post_type, titania::$config->increment_postcount)) { diff --git a/includes/objects/queue.php b/includes/objects/queue.php index 0d9dc6c5b..09c90196d 100644 --- a/includes/objects/queue.php +++ b/includes/objects/queue.php @@ -323,6 +323,12 @@ public function delete() WHERE revision_id = ' . $this->revision_id; phpbb::$db->sql_query($sql); + // Remove any notifications for this queue item + phpbb::$container->get('notification_manager')->delete_notifications( + array('phpbb.titania.notification.type.queue', 'phpbb.titania.notification.type.queue_move'), + $this->queue_id + ); + // Assplode parent::delete(); } @@ -353,13 +359,19 @@ public function move($new_status, \phpbb\titania\tags $tags) 'CATEGORY_NAME' => $to, 'U_VIEW_QUEUE' => $path_helper->strip_url_params($u_view_queue, 'sid'), ); - $this->subscriptions->send_notifications( - ext::TITANIA_QUEUE_TAG, - $new_status, - 'new_contrib_queue_cat', - $vars, - phpbb::$user->data['user_id'] - ); + $this->subscriptions->send_notifications('queue_move', array( + 'item_id' => $this->queue_id, + 'item_parent_id' => $this->contrib_id, + 'watch' => array(array(ext::TITANIA_QUEUE_TAG, $new_status)), + 'exclude_user' => phpbb::$user->data['user_id'], + 'lang_key' => 'NOTIFICATION_TITANIA_QUEUE_MOVE', + 'lang_params' => array($to), + 'reference' => $contrib->contrib_name, + 'url' => $vars['U_VIEW_QUEUE'], + 'email_template' => 'new_contrib_queue_cat', + 'email_vars' => $vars, + 'actor_id' => phpbb::$user->data['user_id'], + )); } public function in_progress() @@ -461,7 +473,17 @@ public function approve($public_notes, $robot_user_id = 0) 'NAME' => $contrib->contrib_name, 'U_VIEW' => $contrib->get_url(), ); - $this->subscriptions->send_notifications(ext::TITANIA_CONTRIB, $this->contrib_id, 'subscribe_notify', $email_vars); + $this->subscriptions->send_notifications('contribution', array( + 'item_id' => $revision->revision_id, + 'item_parent_id' => $this->contrib_id, + 'watch' => array(array(ext::TITANIA_CONTRIB, $this->contrib_id)), + 'lang_key' => 'NOTIFICATION_TITANIA_CONTRIB_UPDATED', + 'lang_params' => array($revision->revision_version), + 'reference' => $contrib->contrib_name, + 'url' => $email_vars['U_VIEW'], + 'email_template' => 'subscribe_notify', + 'email_vars' => $email_vars, + )); $this->trash_queue_topic(); } diff --git a/includes/objects/revision.php b/includes/objects/revision.php index ca80bee86..10c5a6247 100644 --- a/includes/objects/revision.php +++ b/includes/objects/revision.php @@ -297,6 +297,8 @@ public function display($tpl_block = 'revisions', $show_queue = false, $all_vers */ public function submit() { + $notify_subscribers = false; + if (!$this->revision_id) { // Update the contrib_last_update if required here @@ -312,17 +314,8 @@ public function submit() WHERE contrib_id = ' . $this->contrib_id; phpbb::$db->sql_query($sql); - // Subscriptions - $email_vars = array( - 'NAME' => $this->contrib->contrib_name, - 'U_VIEW' => $this->contrib->get_url(), - ); - $this->subscriptions->send_notifications( - ext::TITANIA_CONTRIB, - $this->contrib_id, - 'subscribe_notify', - $email_vars - ); + // Notify the subscribers once the revision row exists + $notify_subscribers = true; } } else if (sizeof($this->phpbb_versions)) @@ -378,6 +371,26 @@ public function submit() { $this->contrib->update_release_topic(); } + + // Subscriptions + if ($notify_subscribers) + { + $email_vars = array( + 'NAME' => $this->contrib->contrib_name, + 'U_VIEW' => $this->contrib->get_url(), + ); + $this->subscriptions->send_notifications('contribution', array( + 'item_id' => $this->revision_id, + 'item_parent_id' => $this->contrib_id, + 'watch' => array(array(ext::TITANIA_CONTRIB, $this->contrib_id)), + 'lang_key' => 'NOTIFICATION_TITANIA_CONTRIB_UPDATED', + 'lang_params' => array($this->revision_version), + 'reference' => $this->contrib->contrib_name, + 'url' => $email_vars['U_VIEW'], + 'email_template' => 'subscribe_notify', + 'email_vars' => $email_vars, + )); + } } /** @@ -590,6 +603,9 @@ public function delete() // $translations = new titania_attachment(TITANIA_TRANSLATION, $this->revision_id); // $attachment->delete_all(); + // Remove any notifications for this revision + phpbb::$container->get('notification_manager')->delete_notifications('phpbb.titania.notification.type.contribution', $this->revision_id); + // Self-destruct parent::delete(); } diff --git a/language/en/notifications.php b/language/en/notifications.php new file mode 100644 index 000000000..d8a016a9a --- /dev/null +++ b/language/en/notifications.php @@ -0,0 +1,57 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +/** +* DO NOT CHANGE +*/ +if (!defined('IN_PHPBB')) +{ + exit; +} + +if (empty($lang) || !is_array($lang)) +{ + $lang = array(); +} + +// DEVELOPERS PLEASE NOTE +// +// All language files should use UTF-8 as their encoding and the files must not contain a BOM. +// +// Placeholders can now contain order information, e.g. instead of +// 'Page %s of %s' you can (and should) write 'Page %1$s of %2$s', this allows +// translators to re-order the output of data while ensuring it remains correct +// +// You do not need this where single placeholders are used, e.g. 'Message %d' is fine +// equally where a string contains only two placeholders which are used to wrap text +// in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine + +$lang = array_merge($lang, array( + 'NOTIFICATION_GROUP_TITANIA' => 'Customisation Database', + + 'NOTIFICATION_TYPE_TITANIA_POSTED' => 'Someone posts in a subscribed topic or support area', + 'NOTIFICATION_TYPE_TITANIA_CONTRIBUTION' => 'A new version of a subscribed contribution is released', + 'NOTIFICATION_TYPE_TITANIA_QUEUE' => 'A watched validation queue changes', + 'NOTIFICATION_TYPE_TITANIA_ATTENTION' => 'Content is reported or awaits approval', + + 'NOTIFICATION_TITANIA_REPLY' => 'Reply from %1$s to the topic:', + 'NOTIFICATION_TITANIA_REPLY_CONTRIB' => 'Reply from %1$s in the support area of %2$s to the topic:', + 'NOTIFICATION_TITANIA_TOPIC' => 'New topic from %1$s:', + 'NOTIFICATION_TITANIA_TOPIC_CONTRIB' => 'New topic from %1$s in the support area of %2$s:', + 'NOTIFICATION_TITANIA_CONTRIB_UPDATED' => 'New version %1$s released for the contribution:', + 'NOTIFICATION_TITANIA_QUEUE_NEW' => 'New queue item:', + 'NOTIFICATION_TITANIA_QUEUE_MOVE' => 'Queue item moved to %1$s:', + 'NOTIFICATION_TITANIA_ATTENTION' => 'Needs attention:', + 'NOTIFICATION_TITANIA_ATTENTION_REPORT' => 'Reported:', + 'NOTIFICATION_TITANIA_ATTENTION_UNAPPROVED' => 'Awaiting approval:', +)); diff --git a/migrations/notifications_integration.php b/migrations/notifications_integration.php new file mode 100644 index 000000000..3e610e6c9 --- /dev/null +++ b/migrations/notifications_integration.php @@ -0,0 +1,138 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\titania\migrations; + +use phpbb\titania\ext; + +/** +* Seed notification preferences for existing subscribers. +* +* The legacy dispatcher emailed every watcher. The notification system's +* default delivery for users without preference rows is the board method only, +* so existing subscribers would silently stop receiving emails. Give every +* current watcher explicit board + email preferences for the notification +* types their subscriptions map to; new subscribers from here on get the +* standard core defaults. +*/ +class notifications_integration extends base +{ + static public function depends_on() + { + return array('\phpbb\titania\migrations\release_1_1_0'); + } + + public function update_data() + { + return array( + array('custom', array(array($this, 'seed_user_notifications'))), + ); + } + + /** + * Seed user_notifications rows chunk by chunk. + * + * @param mixed $start Offset carried between calls by the migrator + * @return mixed True when done, else the next offset + */ + public function seed_user_notifications($start) + { + $limit = 500; + $start = (int) $start; + + $option_map = array( + ext::TITANIA_CONTRIB => 'phpbb.titania.notification.type.contribution', + ext::TITANIA_SUPPORT => 'phpbb.titania.notification.type.posted', + ext::TITANIA_TOPIC => 'phpbb.titania.notification.type.posted', + ext::TITANIA_QUEUE_DISCUSSION => 'phpbb.titania.notification.type.posted', + ext::TITANIA_QUEUE => 'phpbb.titania.notification.type.queue', + ext::TITANIA_QUEUE_TAG => 'phpbb.titania.notification.type.queue', + ext::TITANIA_ATTENTION => 'phpbb.titania.notification.type.attention', + ); + + $watch_table = $this->get_titania_table_prefix() . 'watch'; + + $sql = 'SELECT DISTINCT w.watch_user_id, w.watch_object_type + FROM ' . $watch_table . ' w, ' . $this->table_prefix . 'users u + WHERE w.watch_user_id = u.user_id + AND w.watch_type = ' . \phpbb\titania\subscriptions::EMAIL . ' + ORDER BY w.watch_user_id, w.watch_object_type'; + $result = $this->db->sql_query_limit($sql, $limit, $start); + + $row_count = 0; + $wanted = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + $row_count++; + + if (isset($option_map[$row['watch_object_type']])) + { + $wanted[(int) $row['watch_user_id']][$option_map[$row['watch_object_type']]] = true; + } + } + $this->db->sql_freeresult($result); + + if (!$row_count) + { + return true; + } + + if (!empty($wanted)) + { + // Users who already have a preference row for a type keep it untouched + $sql = 'SELECT user_id, item_type, method + FROM ' . $this->table_prefix . 'user_notifications + WHERE item_id = 0 + AND ' . $this->db->sql_in_set('user_id', array_keys($wanted)) . ' + AND ' . $this->db->sql_in_set('item_type', array_values($option_map)); + $result = $this->db->sql_query($sql); + + $existing = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + $existing[(int) $row['user_id']][$row['item_type']] = true; + } + $this->db->sql_freeresult($result); + + // The board method delivers by default without a row; an explicit + // email row is all that is needed, like the rows user registration + // creates for the post and topic types. + $insert = array(); + foreach ($wanted as $user_id => $options) + { + foreach ($options as $option => $null) + { + if (isset($existing[$user_id][$option])) + { + continue; + } + + $insert[] = array( + 'item_type' => $option, + 'item_id' => 0, + 'user_id' => $user_id, + 'method' => 'notification.method.email', + 'notify' => 1, + ); + } + } + + if (!empty($insert)) + { + $this->db->sql_multi_insert($this->table_prefix . 'user_notifications', $insert); + } + } + + return ($row_count == $limit) ? $start + $limit : true; + } +} diff --git a/notification/type/attention.php b/notification/type/attention.php new file mode 100644 index 000000000..9b9990df7 --- /dev/null +++ b/notification/type/attention.php @@ -0,0 +1,60 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\titania\notification\type; + +/** +* A new attention item (report or content needing approval) was created. +*/ +class attention extends base +{ + /** @var \phpbb\titania\contribution\type\collection */ + protected $type_collection; + + /** + * {@inheritdoc} + */ + static public $notification_option = array( + 'lang' => 'NOTIFICATION_TYPE_TITANIA_ATTENTION', + 'group' => 'NOTIFICATION_GROUP_TITANIA', + ); + + /** + * Set the contribution type collection. + * + * @param \phpbb\titania\contribution\type\collection $type_collection + */ + public function set_type_collection(\phpbb\titania\contribution\type\collection $type_collection) + { + $this->type_collection = $type_collection; + } + + /** + * {@inheritdoc} + */ + public function get_type() + { + return 'phpbb.titania.notification.type.attention'; + } + + /** + * {@inheritdoc} + * + * Mirrors the attention page's own access check. + */ + public function is_available() + { + return $this->auth->acl_gets('u_titania_mod_contrib_mod', 'u_titania_mod_post_mod') + || count($this->type_collection->find_authed('moderate')) > 0; + } +} diff --git a/notification/type/base.php b/notification/type/base.php new file mode 100644 index 000000000..6021167c5 --- /dev/null +++ b/notification/type/base.php @@ -0,0 +1,205 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\titania\notification\type; + +/** +* Base class for Titania notification types. +* +* All Titania subscription events are data driven: the dispatcher passes the +* watch pairs to select the recipients from, the language key and parameters +* for the board notification, the url, and the email template with its +* variables. Subclasses only differ in their type name, their UCP option and +* their availability. +*/ +abstract class base extends \phpbb\notification\type\base +{ + /** @var \phpbb\user_loader */ + protected $user_loader; + + /** + * Set the user loader (used for actor avatars and names) + * + * @param \phpbb\user_loader $user_loader + */ + public function set_user_loader(\phpbb\user_loader $user_loader) + { + $this->user_loader = $user_loader; + } + + /** + * The item type the user's notification preferences are stored under. + * Types sharing a UCP option (via $notification_option['id']) share it. + * + * @return string + */ + protected function get_option_type() + { + if (static::$notification_option !== false && isset(static::$notification_option['id'])) + { + return static::$notification_option['id']; + } + return $this->get_type(); + } + + /** + * {@inheritdoc} + */ + static public function get_item_id($type_data) + { + return (int) $type_data['item_id']; + } + + /** + * {@inheritdoc} + */ + static public function get_item_parent_id($type_data) + { + return (isset($type_data['item_parent_id'])) ? (int) $type_data['item_parent_id'] : 0; + } + + /** + * Find the users subscribed to the given watch pairs. + * + * @param array $type_data Expects 'watch' (array of array(object_type, object_id) + * pairs) and optionally 'exclude_user'. + * @param array $options + * @return array + */ + public function find_users_for_notification($type_data, $options = array()) + { + $options = array_merge(array( + 'ignore_users' => array(), + ), $options); + + // The watch table constant only exists once Titania's common.php ran, + // which is the case on every page that dispatches a notification. + if (empty($type_data['watch']) || !defined('TITANIA_WATCH_TABLE')) + { + return array(); + } + + $sql_objects = array(); + foreach ($type_data['watch'] as $watch) + { + $sql_objects[] = '(watch_object_type = ' . (int) $watch[0] . ' + AND watch_object_id = ' . (int) $watch[1] . ')'; + } + + $sql = 'SELECT watch_user_id + FROM ' . TITANIA_WATCH_TABLE . ' + WHERE (' . implode(' OR ', $sql_objects) . ')' . + ((!empty($type_data['exclude_user'])) ? ' AND watch_user_id <> ' . (int) $type_data['exclude_user'] : ''); + $result = $this->db->sql_query($sql); + + $users = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + $users[(int) $row['watch_user_id']] = true; + } + $this->db->sql_freeresult($result); + + if (empty($users)) + { + return array(); + } + $users = array_keys($users); + sort($users); + + return $this->check_user_notification_options($users, array_merge($options, array( + 'item_type' => $this->get_option_type(), + ))); + } + + /** + * {@inheritdoc} + */ + public function create_insert_array($type_data, $pre_create_data = array()) + { + $this->set_data('lang_key', $type_data['lang_key']); + $this->set_data('lang_params', (isset($type_data['lang_params'])) ? $type_data['lang_params'] : array()); + $this->set_data('reference', (isset($type_data['reference'])) ? $type_data['reference'] : ''); + $this->set_data('url', (isset($type_data['url'])) ? $type_data['url'] : ''); + $this->set_data('email_template', $type_data['email_template']); + $this->set_data('email_vars', (isset($type_data['email_vars'])) ? $type_data['email_vars'] : array()); + $this->set_data('actor_id', (isset($type_data['actor_id'])) ? (int) $type_data['actor_id'] : 0); + + parent::create_insert_array($type_data, $pre_create_data); + } + + /** + * {@inheritdoc} + */ + public function get_title() + { + $params = $this->get_data('lang_params'); + + return $this->language->lang_array($this->get_data('lang_key'), (is_array($params)) ? $params : array()); + } + + /** + * {@inheritdoc} + */ + public function get_reference() + { + $reference = $this->get_data('reference'); + + return ($reference) ? $this->language->lang('NOTIFICATION_REFERENCE', $reference) : ''; + } + + /** + * {@inheritdoc} + */ + public function get_url() + { + return $this->get_data('url'); + } + + /** + * {@inheritdoc} + */ + public function get_email_template() + { + return '@phpbb_titania/' . $this->get_data('email_template'); + } + + /** + * {@inheritdoc} + */ + public function get_email_template_variables() + { + $vars = $this->get_data('email_vars'); + + return (is_array($vars)) ? $vars : array(); + } + + /** + * {@inheritdoc} + */ + public function users_to_query() + { + $actor_id = (int) $this->get_data('actor_id'); + + return ($actor_id) ? array($actor_id) : array(); + } + + /** + * {@inheritdoc} + */ + public function get_avatar() + { + $actor_id = (int) $this->get_data('actor_id'); + + return ($actor_id) ? $this->user_loader->get_avatar($actor_id, false, true) : ''; + } +} diff --git a/notification/type/contribution.php b/notification/type/contribution.php new file mode 100644 index 000000000..3faa13c45 --- /dev/null +++ b/notification/type/contribution.php @@ -0,0 +1,36 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\titania\notification\type; + +/** +* A new revision of a subscribed contribution was released. +*/ +class contribution extends base +{ + /** + * {@inheritdoc} + */ + static public $notification_option = array( + 'lang' => 'NOTIFICATION_TYPE_TITANIA_CONTRIBUTION', + 'group' => 'NOTIFICATION_GROUP_TITANIA', + ); + + /** + * {@inheritdoc} + */ + public function get_type() + { + return 'phpbb.titania.notification.type.contribution'; + } +} diff --git a/notification/type/posted.php b/notification/type/posted.php new file mode 100644 index 000000000..75d3cc23a --- /dev/null +++ b/notification/type/posted.php @@ -0,0 +1,53 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\titania\notification\type; + +/** +* New topics and replies in subscribed topics and support areas. +*/ +class posted extends base +{ + /** + * {@inheritdoc} + */ + static public $notification_option = array( + 'lang' => 'NOTIFICATION_TYPE_TITANIA_POSTED', + 'group' => 'NOTIFICATION_GROUP_TITANIA', + ); + + /** + * {@inheritdoc} + */ + public function get_type() + { + return 'phpbb.titania.notification.type.posted'; + } + + /** + * {@inheritdoc} + * + * The poster's name is resolved at display time through the user loader, + * like core's post notification does. + */ + public function get_title() + { + $params = $this->get_data('lang_params'); + $username = $this->user_loader->get_username((int) $this->get_data('actor_id'), 'no_profile'); + + return $this->language->lang_array( + $this->get_data('lang_key'), + array_merge(array($username), (is_array($params)) ? $params : array()) + ); + } +} diff --git a/notification/type/queue.php b/notification/type/queue.php new file mode 100644 index 000000000..ff1416f98 --- /dev/null +++ b/notification/type/queue.php @@ -0,0 +1,59 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\titania\notification\type; + +/** +* A new item entered a watched validation queue. +*/ +class queue extends base +{ + /** @var \phpbb\titania\contribution\type\collection */ + protected $type_collection; + + /** + * {@inheritdoc} + */ + static public $notification_option = array( + 'lang' => 'NOTIFICATION_TYPE_TITANIA_QUEUE', + 'group' => 'NOTIFICATION_GROUP_TITANIA', + ); + + /** + * Set the contribution type collection. + * + * @param \phpbb\titania\contribution\type\collection $type_collection + */ + public function set_type_collection(\phpbb\titania\contribution\type\collection $type_collection) + { + $this->type_collection = $type_collection; + } + + /** + * {@inheritdoc} + */ + public function get_type() + { + return 'phpbb.titania.notification.type.queue'; + } + + /** + * {@inheritdoc} + * + * Only users who can view at least one validation queue see the option. + */ + public function is_available() + { + return count($this->type_collection->find_authed('view')) > 0; + } +} diff --git a/notification/type/queue_move.php b/notification/type/queue_move.php new file mode 100644 index 000000000..e98591d24 --- /dev/null +++ b/notification/type/queue_move.php @@ -0,0 +1,41 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\titania\notification\type; + +/** +* A queue item moved to a watched queue category. +* +* A separate type from queue only because both events concern the same queue +* item and notifications deduplicate per type and item id; it shares the queue +* type's UCP option and preferences. +*/ +class queue_move extends queue +{ + /** + * {@inheritdoc} + */ + static public $notification_option = array( + 'id' => 'phpbb.titania.notification.type.queue', + 'lang' => 'NOTIFICATION_TYPE_TITANIA_QUEUE', + 'group' => 'NOTIFICATION_GROUP_TITANIA', + ); + + /** + * {@inheritdoc} + */ + public function get_type() + { + return 'phpbb.titania.notification.type.queue_move'; + } +} diff --git a/posting.php b/posting.php index 2c5f32988..ce70f6698 100644 --- a/posting.php +++ b/posting.php @@ -48,6 +48,9 @@ class posting /** @var \phpbb\titania\subscriptions */ protected $subscriptions; + /** @var \phpbb\path_helper */ + protected $path_helper; + /** @var \phpbb\titania\attachment\operator */ protected $attachments; @@ -76,9 +79,10 @@ class posting * @param message $message * @param access $access * @param subscriptions $subscriptions + * @param \phpbb\path_helper $path_helper * @param \phpbb\titania\attachment\operator $attachments */ - public function __construct(\phpbb\auth\auth $auth, \phpbb\db\driver\driver_interface $db, \phpbb\user $user, \phpbb\request\request_interface $request, \phpbb\template\template $template, controller\helper $controller_helper, message $message, access $access, subscriptions $subscriptions, \phpbb\titania\attachment\operator $attachments) + public function __construct(\phpbb\auth\auth $auth, \phpbb\db\driver\driver_interface $db, \phpbb\user $user, \phpbb\request\request_interface $request, \phpbb\template\template $template, controller\helper $controller_helper, message $message, access $access, subscriptions $subscriptions, \phpbb\path_helper $path_helper, \phpbb\titania\attachment\operator $attachments) { $this->auth = $auth; $this->db = $db; @@ -89,6 +93,7 @@ public function __construct(\phpbb\auth\auth $auth, \phpbb\db\driver\driver_inte $this->message = $message; $this->access = $access; $this->subscriptions = $subscriptions; + $this->path_helper = $path_helper; $this->attachments = $attachments; } @@ -1259,16 +1264,18 @@ protected function send_notifications(\titania_post $post, $mode) $email_vars = array( 'NAME' => htmlspecialchars_decode($post->topic->topic_subject), ); + $lang_params = array(); if ($is_support_topic) { $email_vars['CONTRIB_NAME'] = $this->contrib->contrib_name; + $lang_params[] = $this->contrib->contrib_name; } if ($mode == 'reply') { - $object_type = array(ext::TITANIA_TOPIC); - $object_id = array($post->topic_id); + $watch = array(array(ext::TITANIA_TOPIC, $post->topic_id)); + $lang_key = 'NOTIFICATION_TITANIA_REPLY'; $topic_params = array( 'view' => 'unread', '#' => 'unread', @@ -1277,26 +1284,32 @@ protected function send_notifications(\titania_post $post, $mode) if ($is_support_topic) { // Support topic reply - $object_id[] = $post->topic->parent_id; - $object_type[] = ext::TITANIA_SUPPORT; - $template .= '_contrib'; + $watch[] = array(ext::TITANIA_SUPPORT, $post->topic->parent_id); + $template .= '_contrib'; + $lang_key = 'NOTIFICATION_TITANIA_REPLY_CONTRIB'; } } else { - $object_type = $post->post_type; - $object_id = $post->topic->parent_id; - $template .= ($is_support_topic) ? '_forum_contrib' : '_forum'; + $watch = array(array($post->post_type, $post->topic->parent_id)); + $template .= ($is_support_topic) ? '_forum_contrib' : '_forum'; + $lang_key = ($is_support_topic) ? 'NOTIFICATION_TITANIA_TOPIC_CONTRIB' : 'NOTIFICATION_TITANIA_TOPIC'; } $email_vars['U_VIEW'] = $post->topic->get_url(false, $topic_params); - $this->subscriptions->send_notifications( - $object_type, - $object_id, - $template, - $email_vars, - $post->post_user_id - ); + $this->subscriptions->send_notifications('posted', array( + 'item_id' => $post->post_id, + 'item_parent_id' => $post->topic_id, + 'watch' => $watch, + 'exclude_user' => $post->post_user_id, + 'lang_key' => $lang_key, + 'lang_params' => $lang_params, + 'reference' => $post->topic->topic_subject, + 'url' => $email_vars['U_VIEW'], + 'email_template' => $template, + 'email_vars' => $email_vars, + 'actor_id' => $post->post_user_id, + )); } /** diff --git a/subscriptions.php b/subscriptions.php index c0b3ec3be..9b7213afc 100644 --- a/subscriptions.php +++ b/subscriptions.php @@ -18,58 +18,50 @@ class subscriptions /** @var \phpbb\db\driver\driver_interface */ protected $db; - /** @var \phpbb\config\config */ - protected $config; - /** @var \phpbb\request\request_interface */ protected $request; + /** @var \phpbb\template\template */ + protected $template; + /** @var \phpbb\user */ protected $user; /** @var \phpbb\path_helper */ protected $path_helper; - /** @var string */ - protected $users_table; + /** @var \phpbb\notification\manager */ + protected $notification_manager; /** @var string */ protected $watch_table; - /** @var string */ - protected $phpbb_root_path; - - /** @var string */ - protected $php_ext; - + /** + * The historical delivery type stored in watch_type. Every row carries + * EMAIL; delivery preferences now live in the notification system, so the + * column only distinguishes subscription rows, not how they are delivered. + */ const EMAIL = 1; - const WATCH = 2; /** * Constructor * * @param \phpbb\db\driver\driver_interface $db - * @param \phpbb\config\config $config * @param \phpbb\request\request_interface $request * @param \phpbb\template\template $template * @param \phpbb\user $user * @param \phpbb\path_helper $path_helper - * @param string $users_table - * @param string $phpbb_root_path - * @param string $php_ext + * @param \phpbb\notification\manager $notification_manager */ - public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, \phpbb\request\request_interface $request, \phpbb\template\template $template, \phpbb\user $user, \phpbb\path_helper $path_helper, $users_table, $phpbb_root_path, $php_ext) + public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\request\request_interface $request, \phpbb\template\template $template, \phpbb\user $user, \phpbb\path_helper $path_helper, \phpbb\notification\manager $notification_manager) { $this->db = $db; - $this->config = $config; $this->request = $request; $this->template = $template; $this->user = $user; $this->path_helper = $path_helper; - $this->users_table = $users_table; + $this->notification_manager = $notification_manager; $this->watch_table = TITANIA_WATCH_TABLE; - $this->phpbb_root_path = $phpbb_root_path; - $this->php_ext = $php_ext; } /** @@ -201,125 +193,36 @@ public function unsubscribe($object_type, $object_id, $user_id = false) } /** - * Send Notifications - * - * Using this function: - * Call this function when you know the Object type, object id, and the email - * template name. - * Sample usage: - * - * - * - * $object_type = SOME_OBJECT_CONSTANT_TYPE; - * $obhect)id = 242974; + * Send subscription notifications through the phpBB notification system. * - * titania_subscriptions::send_notifications($object_type, $object_id, 'mod_subscribe', array( - * 'OBJECT_NAME' => 'Some MOD', - * )); - * - * - * - * The vars parameter will be used in the messanger assign vars, which will act - * as the common vars when sending out the notifications. Data such as the MOD's - * or Style's name should go here, what action was taken, etc. The usernaeme and - * emails of the recepiants will be personalised by the function. Ensure the - * email template has the {USERNAME} var present. - * - * @param $exclude_user User_id of the one who posted the item to exclude them from the sending + * Watchers of the given watch pairs receive the notification through the + * delivery methods they enabled in the UCP (board and/or email); the email + * method renders the same Titania email templates the legacy dispatcher + * used, with the same variables plus USERNAME added by the core. * + * @param string $type Titania notification type suffix + * (posted|contribution|queue|queue_move|attention) + * @param array $type_data Notification data: + * 'item_id' int the notified item (post, revision, queue, attention id) + * 'item_parent_id' int its parent (optional) + * 'watch' array of array(watch_object_type, watch_object_id) + * pairs selecting the recipients + * 'exclude_user' int user to exclude, normally the acting user (optional) + * 'lang_key' string language key for the board notification title + * 'lang_params' array parameters for the language key (optional) + * 'url' string url the notification links to + * 'email_template' string Titania email template name + * 'email_vars' array variables for the email template (optional) + * 'actor_id' int user shown as the notification's actor (optional) */ - public function send_notifications($object_type, $object_id, $email_tpl, $vars, $exclude_user = false) + public function send_notifications($type, array $type_data) { - $sql = 'SELECT w.watch_user_id, w.watch_type, u.user_id, u.username, u.user_email, u.user_lang - FROM ' . $this->watch_table . ' w, ' . $this->users_table . ' u - WHERE w.watch_user_id = u.user_id '; - - if (is_array($object_type) || is_array($object_id)) - { - // Both needs to be arrays if one is and they need to have the same number of elements. - if (!is_array($object_type) || !is_array($object_id) || sizeof($object_type) != sizeof($object_id)) - { - return; - } - - $sql_objects = ''; - foreach ($object_type as $key => $value) - { - $sql_objects .= (($sql_objects == '') ? '' : ' OR ') . '(w.watch_object_type = ' . (int) $value . ' - AND w.watch_object_id = ' . (int) $object_id[$key] . ')'; - } - $sql .= 'AND (' . $sql_objects . ')'; - - unset($sql_objects); - } - else - { - $sql .= 'AND w.watch_object_type = ' . (int) $object_type . ' - AND w.watch_object_id = ' . (int) $object_id; - } - $sql .= ($exclude_user) ? ' AND w.watch_user_id <> ' . (int) $exclude_user : ''; - - $result = $this->db->sql_query($sql); - - // Throw everything here - $user_data = array(); - while ($row = $this->db->sql_fetchrow($result)) - { - // Use user_id for the keys to not send duplicates. - $user_data[$row['user_id']] = array( - 'username' => $row['username'], - 'user_email' => $row['user_email'], - 'user_lang' => $row['user_lang'], - 'watch_type' => $row['watch_type'], - ); - } - $this->db->sql_freeresult($result); - - // No one subscribed? We're done. - if (empty($user_data)) - { - return; - } - $messenger = null; - - // Send to each user - // Add a new case statment for each subscription type - foreach ($user_data as $data) + // A stored notification url must never carry a session id + if (!empty($type_data['url'])) { - /* - * Switch between the types. - * ------------------------------------------ - * When adding a type, the final message will - * be stored in $message, and the subject is - * stored in $vars['SUBJECT']. - */ - switch($data['watch_type']) - { - case self::EMAIL: - - if ($messenger === null) - { - // Only make the object if we need it - if (!class_exists('\messenger')) - { - require($this->phpbb_root_path . 'includes/functions_messenger.' . $this->php_ext); - } - $messenger = new \messenger; - } - - $messenger->anti_abuse_headers($this->config, $this->user); - $messenger->template('@phpbb_titania/' . $email_tpl, $data['user_lang']); - $messenger->to($data['user_email'], $data['username']); - $messenger->assign_vars(array_merge($vars, array( - 'USERNAME' => $data['username'], - ))); - - $messenger->send(); - $messenger->save_queue(); - break; - } + $type_data['url'] = $this->path_helper->strip_url_params($type_data['url'], 'sid'); } - return; + $this->notification_manager->add_notifications('phpbb.titania.notification.type.' . $type, $type_data); } }