diff --git a/classes/adapters/email.php b/classes/adapters/email.php
index f47c959b0..c10890a75 100644
--- a/classes/adapters/email.php
+++ b/classes/adapters/email.php
@@ -3,30 +3,57 @@
class WP_Stream_Notification_Adapter_Email extends WP_Stream_Notification_Adapter {
public static function register( $title = '' ) {
- parent::register( __( 'Email', 'stream_notification' ) );
+ parent::register( __( 'Email', 'stream-notifications' ) );
}
public static function fields() {
return array(
- 'to' => array(
- 'title' => __( 'To', 'stream_notification' ),
+ 'users' => array(
+ 'title' => __( 'To users', 'stream-notifications' ),
'type' => 'hidden',
'multiple' => true,
'ajax' => true,
'key' => 'author',
- ),
+ ),
+ 'emails' => array(
+ 'title' => __( 'To emails', 'stream-notifications' ),
+ 'type' => 'text',
+ 'tags' => true,
+ ),
'subject' => array(
- 'title' => __( 'Subject', 'stream_notification' ),
+ 'title' => __( 'Subject', 'stream-notifications' ),
'type' => 'text',
- 'hint' => __( 'ex: "%%summary%%" or "[%%created%% - %%author%%] %%summary%%", consult FAQ for documentaion.', 'stream_notification' ),
- ),
+ 'hint' => __( 'ex: "%%summary%%" or "[%%created%% - %%author%%] %%summary%%", consult FAQ for documentaion.', 'stream-notifications' ),
+ ),
'message' => array(
- 'title' => __( 'Message', 'stream_notification' ),
+ 'title' => __( 'Message', 'stream-notifications' ),
'type' => 'textarea',
- ),
+ ),
);
}
+ public function send( $log ) {
+ $users = $this->params['users'];
+ $user_emails = array();
+ if ( $users ) {
+ $user_query = new WP_User_Query(
+ array(
+ 'include' => $users,
+ 'fields' => array( 'user_email' ),
+ )
+ );
+ $user_emails = wp_list_pluck( $user_query->results, 'user_email' );
+ }
+ $emails = explode( ',', $this->params['emails'] );
+ if ( ! empty( $user_emails ) ) {
+ $emails = array_merge( $emails, $user_emails );
+ }
+ $emails = array_filter( $emails );
+ $subject = $this->replace( $this->params['subject'], $log );
+ $message = $this->replace( $this->params['message'], $log );
+ wp_mail( $to, $subject, $message );
+ }
+
}
WP_Stream_Notification_Adapter_Email::register();
diff --git a/classes/wp-stream-notification-adapter.php b/classes/wp-stream-notification-adapter.php
index 5dc6f1db4..c6ba179f7 100644
--- a/classes/wp-stream-notification-adapter.php
+++ b/classes/wp-stream-notification-adapter.php
@@ -2,6 +2,8 @@
abstract class WP_Stream_Notification_Adapter {
+ public $params = array();
+
public static function register( $title ) {
$class = get_called_class();
$name = strtolower( str_replace( 'WP_Stream_Notification_Adapter_', '', $class ) );
@@ -12,8 +14,90 @@ public static function fields() {
return array();
}
- function send( $log ) {
+ /**
+ * Replace placeholders in alert[field]s with proper info from the log
+ * @param string $haystack Text to replace in
+ * @param array $log Log array
+ * @return string
+ */
+ public static function replace( $haystack, $log ) {
+ if ( preg_match_all( '#%%([^%]+)%%#', $haystack, $placeholders ) ) {
+
+ foreach ( $placeholders[1] as $placeholder ) {
+ $value = false;
+ switch ( $placeholder ) {
+ case 'summary':
+ case 'object_id':
+ case 'author':
+ case 'created':
+ $value = $log[$placeholder];
+ break;
+ case ( strpos( $placeholder, 'meta.' ) !== false ):
+ $meta_key = substr( $placeholder, 5 );
+ if ( isset( $log['meta'][ $meta_key ] ) ) {
+ $value = $log['meta'][ $meta_key ];
+ }
+ break;
+ case ( strpos( $placeholder, 'author.' ) !== false ):
+ $meta_key = substr( $placeholder, 7 );
+ $author = get_userdata( $log['author'] );
+ if ( $author && isset( $author->{$meta_key} ) ) {
+ $value = $author->{$meta_key};
+ }
+ break;
+ // TODO Move this part to Stream base, and abstract it
+ case ( strpos( $placeholder, 'object.' ) !== false ):
+ $meta_key = substr( $placeholder, 7 );
+ $context = key( $log['contexts'] );
+ // can only guess the object type, since there is no
+ // actual reference here
+ switch ( $context ) {
+ case 'post':
+ case 'page':
+ case 'media':
+ $object = get_post( $log['object_id'] );
+ break;
+ case 'users':
+ $object = get_userdata( $log['object_id'] );
+ break;
+ case 'comment':
+ $object = get_comment( $log['object_id'] );
+ break;
+ case 'term':
+ case 'category':
+ case 'post_tag':
+ case 'link_category':
+ $object = get_term( $log['object_id'], $log['meta']['taxonomy'] );
+ break;
+ default:
+ $object = apply_filters( 'stream_notifications_record_object', $log['object_id'], $log );
+ break;
+ }
+ if ( is_object( $object ) && isset( $object->{$meta_key} ) ) {
+ $value = $object->{$meta_key};
+ }
+ break;
+ }
+ if ( $value ) {
+ $haystack = str_replace( "%%$placeholder%%", $value, $haystack );
+ }
+ }
+ }
+ return $haystack;
+ }
+ function load( $alert ) {
+ $params = array();
+ $fields = $this::fields();
+ foreach ( $fields as $field => $options ) {
+ $params[ $field ] = isset( $alert[ $field ] )
+ ? $alert[ $field ]
+ : null;
+ }
+ $this->params = $params;
+ return $this;
}
+ abstract function send( $log );
+
}
diff --git a/classes/wp-stream-notification-rule-match.php b/classes/wp-stream-notification-rule-match.php
new file mode 100644
index 000000000..dc18c3d2c
--- /dev/null
+++ b/classes/wp-stream-notification-rule-match.php
@@ -0,0 +1,329 @@
+rules( true );
+ }
+
+ public function rules( $force_refresh = false ) {
+ # DEBUG
+ $force_refresh = true;
+ // Check if we have a valid cache
+ if ( ! $force_refresh && false !== ( $rules = get_transient( self::CACHE_KEY ) ) ) {
+ return $rules;
+ }
+
+ // Get rules
+ $args = array(
+ 'type' => 'notification_rule',
+ 'ignore_context' => true,
+ 'records_per_page' => -1,
+ 'fields' => 'ID',
+ 'visibility' => 'active', // Active rules only
+ );
+ $rules = stream_query( $args );
+ $rules = wp_list_pluck( $rules, 'ID' );
+
+ $rules = $this->format( $rules );
+
+ // Cache the new rules
+ set_transient( self::CACHE_KEY, $rules );
+ return $rules;
+ }
+
+ public function match( $record_id, $log ) {
+
+ $rules = $this->rules();
+ $rule_match = array();
+
+ foreach ( $rules as $rule_id => $rule ) {
+ $rule_match[ $rule_id ] = $this->match_group( $rule['triggers'], $log );
+ }
+
+ $rule_match = array_keys( array_filter( $rule_match ) );
+ $matching_rules = array_intersect_key( $rules, array_flip( $rule_match ) );
+
+ $this->alert( $matching_rules, $log );
+ }
+
+ /**
+ * Match a group of chunked triggers against a log operation
+ * @param array $chunks Chunks of triggers, usually from group[triggers]
+ * @param array $log Log operation array
+ * @return bool Matching result
+ */
+ private function match_group( $chunks, $log ) {
+ // Separate triggers by 'AND'/'OR' relation, to be able to fail early
+ // and not have to traverse the whole trigger tree
+ foreach ( $chunks as $chunk ) {
+ $results = array();
+ foreach ( $chunk as $trigger ) {
+ $is_group = isset( $trigger['triggers'] );
+
+ if ( $is_group ) {
+ $results[] = $this->match_group( $trigger['triggers'], $log );
+ } else {
+ $results[] = $this->match_trigger( $trigger, $log );
+ }
+ }
+ // If the whole chunk fails, fail the whole group
+ if ( count( array_filter( $results ) ) == 0 ) {
+ return false;
+ }
+ }
+ // If nothing fails, group matches
+ return true;
+ }
+
+ public function match_trigger( $trigger, $log ) {
+ $needle = $trigger['value'];
+ $operator = $trigger['operator'];
+ $negative = ( $operator[0] == '!' );
+
+ switch ( $trigger['type'] ) {
+ case 'search':
+ $haystack = $log['summary'];
+ break;
+ case 'object_id':
+ $haystack = $log['object_id'];
+ case 'author':
+ $haystack = $log['author'];
+ case 'author_role':
+ $user = get_userdata( $log['author'] );
+ $haystack = ( $user->exists() && $user->roles ) ? $user->roles[0] : false;
+ break;
+ case 'ip':
+ $haystack = $log['ip'];
+ break;
+ case 'date':
+ $haystack = date( 'Ymd', $log['created'] );
+ $needle = date( 'Ymd', strtotime( $needle ) );
+ break;
+ case 'connector':
+ $haystack = $log['connector'];
+ break;
+ case 'context':
+ $haystack = key( $log['contexts'] );
+ break;
+ case 'action':
+ $haystack = reset( $log['contexts'] );
+ break;
+ }
+
+ $match = false;
+ switch ( $trigger['operator'] ) {
+ case '=':
+ case '!=':
+ case '>=':
+ case '<=':
+ $match = ( $haystack == $needle );
+ case 'in':
+ case '!in':
+ $match = array_filter(
+ (array) $needle,
+ function( $value ) use ( $haystack ) {
+ return $value == $haystack;
+ }
+ );
+ break;
+ // string special comparison operators
+ case 'contains':
+ case '!contains':
+ $match = ( false !== strpos( $haystack, $needle ) );
+ break;
+ case 'regex':
+ $match = preg_match( $needle, $haystack ) > 0;
+ break;
+ // date operators
+ case '<':
+ case '<=':
+ $match = $match || ( $haystack < $needle );
+ break;
+ case '>':
+ case '>=':
+ $match = $match || ( $haystack > $needle );
+ break;
+ }
+ $result = ( $match == ! $negative );
+
+ return $result;
+ }
+
+ /**
+ * Format rules to be usable during the matching process
+ * @param array $rules Array of rule IDs
+ * @return array Reformatted array of groups/triggers
+ */
+ private function format( $rules ) {
+ $output = array();
+ foreach ( $rules as $rule_id ) {
+ $output[ $rule_id ] = array();
+ $rule = new WP_Stream_Notification_Rule( $rule_id );
+
+ // Generate an easy-to-parse tree of triggers/groups
+ $triggers = $this->generate_tree(
+ $this->generate_flattened_tree(
+ $rule->triggers,
+ $rule->groups
+ )
+ );
+
+ // Chunkify! @see generate_group_chunks
+ $output[ $rule_id ]['triggers'] = $this->generate_group_chunks(
+ $triggers[0]['triggers']
+ );
+
+ // Add alerts
+ $output[ $rule_id ]['alerts'] = $rule->alerts;
+ }
+ return $output;
+ }
+
+ /**
+ * Return all of group's ancestors starting with the root
+ */
+ private function generate_group_chain( $groups, $group_id ) {
+ $chain = array();
+ while ( isset( $groups[ $group_id ] ) ) {
+ $chain[] = $group_id;
+ $group_id = $groups[ $group_id ]['group'];
+ }
+ return array_reverse( $chain );
+ }
+
+ /**
+ * Takes the groups and triggers and creates a flattened tree,
+ * which is an pre-order walkthrough of the tree we want to construct
+ * http://en.wikipedia.org/wiki/Tree_traversal#Pre-order
+ */
+ private function generate_flattened_tree( $triggers, $groups ) {
+ // Seed the tree with the universal group
+ if ( ! isset( $groups[0] ) ) {
+ $groups[0] = array( 'group' => null, 'relation' => 'and' );
+ }
+ $flattened_tree = array( array( 'item' => $groups['0'], 'level' => 0, 'type' => 'group' ) );
+ $current_group_chain = array( '0' );
+ $level = 1;
+
+ foreach ( $triggers as $key => $trigger ) {
+ $active_group = end( $current_group_chain );
+
+ // If the trigger goes to any other than actually opened group, we need to traverse the tree first
+ if ( $trigger['group'] != $active_group ) {
+
+ $trigger_group_chain = $this->generate_group_chain( $groups, $trigger['group'] );
+ $common_ancestors = array_intersect( $current_group_chain, $trigger_group_chain );
+ $newly_inserted_groups = array_diff( $trigger_group_chain, $current_group_chain );
+ $steps_back = $level - count( $common_ancestors );
+
+ // First take the steps back until we reach a common ancestor
+ for ( $i = 0; $i < $steps_back; $i++ ) {
+ array_pop( $current_group_chain );
+ $level--;
+ }
+
+ // Then go forward and generate group nodes until the trigger is ready to be inserted
+ foreach ( $newly_inserted_groups as $group ) {
+ $flattened_tree[] = array( 'item' => $groups[ $group ], 'level' => $level++, 'type' => 'group' );
+ $current_group_chain[] = $group;
+ }
+ }
+ // Now we're sure the trigger goes to a correct position
+ $flattened_tree[] = array( 'item' => $trigger, 'level' => $level, 'type' => 'trigger' );
+ }
+
+ return $flattened_tree;
+ }
+
+ /**
+ * Takes the flattened tree and generates a proper tree
+ */
+ private function generate_tree( $flattened_tree ) {
+ // Our recurrent step
+ $recurrent_step = function( $level, $i ) use ( $flattened_tree, &$recurrent_step ) {
+ $return = array();
+ for ( $i; $i < count( $flattened_tree ); $i++ ) {
+ // If we're on the correct level, we're going to insert the node
+ if ( $flattened_tree[$i]['level'] == $level ) {
+ if ( $flattened_tree[$i]['type'] == 'trigger' ) {
+ $return[] = $flattened_tree[$i]['item'];
+ // If the node is a group, we need to call the recursive function
+ // in order to construct the tree for us further
+ } else {
+ $return[] = array(
+ 'relation' => $flattened_tree[$i]['item']['relation'],
+ 'triggers' => call_user_func( $recurrent_step, $level + 1, $i + 1 ),
+ );
+ }
+ // If we're on a lower level, we came back and we can return this branch
+ } elseif ( $flattened_tree[$i]['level'] < $level ) {
+ return $return;
+ }
+ }
+ return $return;
+ };
+ return call_user_func( $recurrent_step, 0, 0 );
+ }
+
+ /**
+ * Split trigger trees by relation, so we can fail trigger trees early if
+ * an effective trigger is not matched
+ *
+ * A chunk would be a bulk of triggers that only matches if ANY of its
+ * nested triggers are matched
+ *
+ * @param array $group Group array, ex: array(
+ * 'relation' => 'and',
+ * 'trigger' => array( arr trigger1, arr trigger2 )
+ * );
+ * @return array Chunks of triggers, split based on their relation
+ */
+ private function generate_group_chunks( $triggers ) {
+ $chunks = array();
+ $current_chunk = -1;
+ foreach ( $triggers as $trigger ) {
+ // If is a group, chunks its children as well
+ if ( isset( $trigger['triggers'] ) ) {
+ $trigger['triggers'] = $this->generate_group_chunks( $trigger['triggers'] );
+ }
+ // If relation=and, start a new chunk, else join the previous chunk
+ if ( $trigger['relation'] == 'and' ) {
+ $chunks[] = array( $trigger );
+ $current_chunk = count( $chunks ) - 1;
+ } else {
+ $chunks[ $current_chunk ][] = $trigger;
+ }
+ }
+ return $chunks;
+ }
+
+ private function alert( $rules, $log ) {
+ foreach ( $rules as $rule_id => $rule ) {
+ // Update occurrences
+ update_stream_meta(
+ $rule_id,
+ 'occurrences',
+ ( (int) get_stream_meta( $rule_id, 'occurrences', true ) ) + 1
+ );
+ foreach ( $rule['alerts'] as $alert ) {
+ if ( ! isset( WP_Stream_Notifications::$adapters[$alert['type']] ) ) {
+ continue;
+ }
+ $adapter = new WP_Stream_Notifications::$adapters[$alert['type']]['class'];
+ $adapter->load( $alert )->send( $log );
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/classes/wp-stream-notification-rule.php b/classes/wp-stream-notification-rule.php
index d3c53d446..279443ebd 100644
--- a/classes/wp-stream-notification-rule.php
+++ b/classes/wp-stream-notification-rule.php
@@ -7,12 +7,12 @@ class WP_Stream_Notification_Rule {
private $summary;
private $visibility;
private $created;
-
- private $type = 'notification_rule';
+
+ private $type = 'notification_rule';
private $triggers = array();
- private $groups = array();
- private $alerts = array();
+ private $groups = array();
+ private $alerts = array();
function __construct( $id = null ) {
if ( $id ) {
@@ -22,10 +22,12 @@ function __construct( $id = null ) {
function load( $id ) {
global $wpdb;
- $item = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->stream WHERE type = 'notification_rule' AND ID = %d", $id ) );
+ $item = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->stream WHERE type = 'notification_rule' AND ID = %d", $id ) ); // cache ok, db call ok
if ( $item ) {
$meta = get_option( 'stream_notifications_' . $item->ID );
- if ( ! $meta || ! is_array( $meta ) ) $meta = array();
+ if ( ! $meta || ! is_array( $meta ) ) {
+ $meta = array();
+ }
$this->load_from_array( array_merge( (array) $item, $meta ) );
}
return $this;
@@ -48,26 +50,33 @@ function exists() {
function save() {
global $wpdb;
- $defaults = array(
- 'ID' => null,
- 'author' => wp_get_current_user()->ID,
- 'summary' => null,
- 'visibility' => 0,
- 'type' => 'notfication_rule',
- 'created' => current_time( 'r', 1 ),
+ $defaults = array(
+ 'ID' => null,
+ 'author' => wp_get_current_user()->ID,
+ 'summary' => null,
+ 'visibility' => 'inactive',
+ 'type' => 'notfication_rule',
+ 'created' => current_time( 'mysql', 1 ),
);
$data = $this->to_array();
$record = array_intersect_key( $data, $defaults );
if ( $this->exists() ) {
- $result = $wpdb->update( $wpdb->stream, $record, array( 'ID' => $this->ID ) );
+ $result = $wpdb->update( $wpdb->stream, $record, array( 'ID' => $this->ID ) ); // cache ok, db call ok
+ // Reset occurrences
+ update_stream_meta( $record['ID'], 'occurrences', 0 );
$success = ( $result !== false );
} else {
+ if ( ! $record['created'] ) {
+ unset( $record['created'] );
+ }
$record = wp_parse_args( $record, $defaults );
- $result = $wpdb->insert( $wpdb->stream, $record );
+ $result = $wpdb->insert( $wpdb->stream, $record ); // cache ok, db call ok
$success = ( is_int( $result ) );
- if ( $success ) $this->ID = $wpdb->insert_id;
+ if ( $success ) {
+ $this->ID = $wpdb->insert_id; // cache ok, db call ok
+ }
}
if ( $this->ID ) {
@@ -96,4 +105,4 @@ function __get( $key ) {
return $r;
}
-}
\ No newline at end of file
+}
diff --git a/includes/list-table.php b/includes/list-table.php
new file mode 100644
index 000000000..ff08ed429
--- /dev/null
+++ b/includes/list-table.php
@@ -0,0 +1,517 @@
+ 'stream_notifications',
+ 'plural' => 'rules',
+ 'screen' => isset( $args['screen'] ) ? $args['screen'] : null,
+ )
+ );
+
+ add_screen_option(
+ 'per_page',
+ array(
+ 'default' => 20,
+ 'label' => __( 'Rules per page', 'stream-notifications' ),
+ 'option' => 'edit_stream_notifications_per_page',
+ )
+ );
+
+ add_filter( 'set-screen-option', array( __CLASS__, 'set_screen_option' ), 10, 3 );
+ set_screen_options();
+ }
+
+ function extra_tablenav( $which ) {
+ $this->filters_form( $which );
+ }
+
+ function get_columns(){
+ return apply_filters(
+ 'wp_stream_notifications_list_table_columns',
+ array(
+ 'cb' => '',
+ 'name' => __( 'Name', 'stream-notifications' ),
+ 'type' => __( 'Type', 'stream-notifications' ),
+ 'occurences' => __( 'Occurences', 'stream-notifications' ),
+ 'created' => __( 'Created', 'stream-notifications' ),
+ )
+ );
+ }
+
+ function get_sortable_columns() {
+ return array(
+ 'created' => 'created',
+ );
+ }
+
+ function prepare_items() {
+ $columns = $this->get_columns();
+ $sortable = $this->get_sortable_columns();
+ $hidden = get_hidden_columns( $this->screen );
+
+ $this->_column_headers = array( $columns, $hidden, $sortable );
+
+ $this->items = $this->get_records();
+
+ $total_items = $this->get_total_found_rows();
+
+ $this->set_pagination_args(
+ array(
+ 'total_items' => $total_items,
+ 'per_page' => $this->get_items_per_page( 'edit_stream_notifications_per_page', 20 ),
+ )
+ );
+ }
+
+ /**
+ * Render the checkbox column
+ *
+ * @param array $item Contains all the data for the checkbox column
+ * @return string Displays a checkbox
+ */
+ function column_cb( $item ) {
+ return sprintf(
+ '',
+ /*$1%s*/ 'wp_stream_notifications_checkbox',
+ /*$2%s*/ $item->ID
+ );
+ }
+
+ function count_records( $args = array() ) {
+ $defaults = array(
+ 'records_per_page' => 1,
+ 'ignore_url_params' => true,
+ );
+
+ $args = wp_parse_args( $args, $defaults );
+ $records = $this->get_records( $args );
+
+ return $this->get_total_found_rows();
+ }
+
+ function get_records( $args = array() ) {
+
+ $defaults = array(
+ 'ignore_url_params' => false,
+ );
+ $args = wp_parse_args( $args, $defaults );
+
+ // Parse sorting params
+ if ( ! $order = filter_input( INPUT_GET, 'order' ) ) {
+ $order = 'DESC';
+ }
+ if ( ! $orderby = filter_input( INPUT_GET, 'orderby' ) ) {
+ $orderby = '';
+ }
+ $args['order'] = $order;
+ $args['orderby'] = $orderby;
+ $args['paged'] = $this->get_pagenum();
+ $args['type'] = 'notification_rule';
+ $args['ignore_context'] = true;
+
+ if ( ! $args['ignore_url_params'] ) {
+ $allowed_params = array(
+ 'search',
+ 'visibility',
+ );
+
+ foreach ( $allowed_params as $param ) {
+ if ( $paramval = filter_input( INPUT_GET, $param ) ) {
+ $args[$param] = $paramval;
+ }
+ }
+ }
+
+ if ( ! isset( $args['records_per_page'] ) ) {
+ $args['records_per_page'] = $this->get_items_per_page( 'edit_stream_notifications_per_page', 20 );
+ }
+
+ $items = stream_query( $args );
+ return $items;
+ }
+
+ function get_total_found_rows() {
+ global $wpdb;
+ return $wpdb->get_var( 'SELECT FOUND_ROWS()' ); // db call ok, cache ok
+ }
+
+ function column_default( $item, $column_name ) {
+ switch ( $column_name ) {
+
+ case 'name':
+ $name = $item->summary
+ ? $item->summary
+ : '(' . __( 'no title', 'stream' ) . ')';
+
+ $out = sprintf(
+ '%s',
+ admin_url( sprintf( 'admin.php?page=wp_stream_notifications&view=rule&action=edit&id=%s', $item->ID ) ),
+ 'row-title',
+ esc_attr( $name ),
+ esc_html( $name )
+ );
+
+ $out .= $this->get_action_links( $item );
+ break;
+
+ case 'type':
+ $out = $this->get_rule_types( $item );
+ break;
+
+ case 'occurences':
+ $out = (int) get_stream_meta( $item->ID, 'occurrences', true );
+ break;
+
+ case 'created':
+ $out = $this->column_link( get_date_from_gmt( $item->created, 'Y/m/d' ), 'date', date( 'Y/m/d', strtotime( $item->created ) ) );
+ $out .= '
';
+ $out .= 'active' == $item->visibility
+ ? __( 'Active', 'stream-notifications' )
+ : __( 'Inactive', 'stream-notifications' );
+ break;
+
+ default:
+ // Register inserted column defaults. Must match a column header from get_columns.
+ $inserted_columns = apply_filters( 'wp_stream_notifications_register_column_defaults', $new_columns = array() );
+
+ if ( ! empty( $inserted_columns ) && is_array( $inserted_columns ) ) {
+ foreach ( $inserted_columns as $column_title ) {
+ /**
+ * If column title inserted via wp_stream_notifications_register_column_defaults ($column_title) exists
+ * among columns registered with get_columns ($column_name) and there is an action associated
+ * with this column, do the action
+ *
+ * Also, note that the action name must include the $column_title registered
+ * with wp_stream_notifications_register_column_defaults
+ */
+ if ( $column_title == $column_name && has_action( 'wp_stream_notifications_insert_column_default-' . $column_title ) ) {
+ $out = do_action( 'wp_stream_notifications_insert_column_default-' . $column_title, $item );
+ } else {
+ $out = $column_name;
+ }
+ }
+ } else {
+ $out = $column_name; // xss okay
+ }
+ break;
+ }
+
+ echo $out; // xss okay
+ }
+
+
+ public static function get_action_links( $record ){
+ $out = '';
+ $custom_links = apply_filters( 'wp_stream_notifications_custom_action_links_' . $record->ID, array(), $record );
+
+ $out .= '
';
+
+ $activation_nonce = wp_create_nonce( "activate-record_$record->ID" );
+ $deletion_nonce = wp_create_nonce( "delete-record_$record->ID" );
+
+ $action_links = array();
+ $action_links[ __( 'Edit', 'stream-notifications' ) ] = array(
+ 'href' => admin_url( sprintf( 'admin.php?page=wp_stream_notifications&view=rule&action=edit&id=%s', $record->ID ) ),
+ 'class' => null,
+ );
+
+ if ( 'active' == $record->visibility ) {
+ $action_links[ __( 'Deactivate', 'stream-notifications' ) ] = array(
+ 'href' => admin_url( sprintf( 'admin.php?page=wp_stream_notifications&action=deactivate&id=%s&wp_stream_nonce=%s', $record->ID, $activation_nonce ) ),
+ 'class' => null,
+ );
+ } elseif ( 'inactive' == $record->visibility ) {
+ $action_links[ __( 'Activate', 'stream-notifications' ) ] = array(
+ 'href' => admin_url( sprintf( 'admin.php?page=wp_stream_notifications&action=activate&id=%s&wp_stream_nonce=%s', $record->ID, $activation_nonce ) ),
+ 'class' => null,
+ );
+ $action_links[ __( 'Delete Permanently', 'stream-notifications' ) ] = array(
+ 'href' => admin_url( sprintf( 'admin.php?page=wp_stream_notifications&action=delete&id=%s&wp_stream_nonce=%s', $record->ID, $deletion_nonce ) ),
+ 'class' => 'delete',
+ );
+ }
+
+ if ( $action_links ) {
+ $links = array();
+ $i = 0;
+ foreach ( $action_links as $link_title => $link_options ) {
+ $i++;
+ $links[] = sprintf(
+ '
%s%s',
+ $link_options['class'],
+ $link_options['href'],
+ $link_title,
+ ( $i === count( $action_links ) ) ? null : ' | '
+ );
+ }
+ $out .= implode( '', $links );
+ }
+
+ if ( $custom_links ) {
+ $out .= ' | ';
+ }
+
+ if ( $custom_links && is_array( $custom_links ) ) {
+ $last_link = end( $custom_links );
+ foreach ( $custom_links as $key => $link ) {
+ $out .= $link;
+ if ( $key != $last_link ) {
+ $out .= ' | ';
+ }
+ }
+ }
+
+ $out .= '
';
+
+ return $out;
+ }
+
+ function column_link( $display, $key, $value = null, $title = null, $class = null ) {
+ $url = admin_url( 'admin.php?page=' . WP_Stream_Notifications::NOTIFICATIONS_PAGE_SLUG );
+
+ $args = ! is_array( $key ) ? array( $key => $value ) : $key;
+
+ foreach ( $args as $k => $v ) {
+ $url = add_query_arg( $k, $v, $url );
+ }
+
+ return sprintf(
+ '%s',
+ esc_url( $url ),
+ esc_attr( $class ),
+ esc_attr( $title ),
+ esc_html( $display )
+ );
+ }
+
+ function filters_form( $which ) {
+ if ( 'top' == $which ) {
+ $filters_string = sprintf(
+ '',
+ WP_Stream_Notifications::NOTIFICATIONS_PAGE_SLUG,
+ wp_create_nonce( 'wp_stream_notifications_bulk_actions' )
+ );
+
+ echo sprintf(
+ '%s
+
+ %s
+
+
+ %s
+
',
+ $this->filter_search(),
+ $this->stream_notifications_bulk_actions( $which ),
+ $filters_string
+ ); // xss okay
+ } else {
+ echo sprintf(
+ '
+ %s
+
',
+ $this->stream_notifications_bulk_actions( $which )
+ ); // xss okay
+ }
+ }
+
+ /**
+ * Return the bulk actions select box, context aware
+ *
+ * @todo Should we utilize WP_List_Table->bulk_actions()?
+ * @param string $which Indicates whether to display the box over or under the list [top|bottom]
+ * @return string Bulk actions select box and a respective submit
+ */
+ function stream_notifications_bulk_actions( $which ) {
+ $dropdown_name = ( 'top' == $which ) ? 'action' : 'action2';
+ $visibility = filter_input( INPUT_GET, 'visibility', FILTER_DEFAULT );
+ $options = array();
+
+ $options[] = sprintf(
+ '',
+ esc_html__( 'Bulk Actions', 'stream-notifications' )
+ );
+
+ if ( 'active' != $visibility ) {
+ $options[] = sprintf(
+ '',
+ esc_html__( 'Activate', 'stream-notifications' )
+ );
+ }
+ if ( 'inactive' != $visibility ) {
+ $options[] = sprintf(
+ '',
+ esc_html__( 'Deactivate', 'stream-notifications' )
+ );
+ }
+ if ( 'inactive' == $visibility ) {
+ $options[] = sprintf(
+ '',
+ esc_html__( 'Delete Permanently', 'stream-notifications' )
+ );
+ }
+
+ $options = apply_filters( 'wp_stream_notifications_bulk_action_options', $options, $which, $visibility );
+ $options_html = implode( '', $options );
+
+ $html = sprintf(
+ '
+ ',
+ $dropdown_name,
+ $options_html,
+ esc_attr__( 'Apply', 'stream-notifications' )
+ );
+
+ return apply_filters( 'wp_stream_notifications_bulk_actions_html', $html );
+ }
+
+ function list_navigation() {
+ $navigation_items = array(
+ 'all' => array(
+ 'link_text' => __( 'All', 'stream-notifications' ),
+ 'url' => admin_url( sprintf( 'admin.php?page=%s', WP_Stream_Notifications::NOTIFICATIONS_PAGE_SLUG ) ),
+ 'link_class' => null,
+ 'li_class' => null,
+ 'count' => $this->count_records(),
+ ),
+ 'active' => array(
+ 'link_text' => __( 'Active', 'stream-notifications' ),
+ 'url' => admin_url( sprintf( 'admin.php?page=%s&visibility=active', WP_Stream_Notifications::NOTIFICATIONS_PAGE_SLUG ) ),
+ 'link_class' => null,
+ 'li_class' => null,
+ 'count' => $this->count_records( array( 'visibility' => 'active' ) ),
+ ),
+ 'inactive' => array(
+ 'link_text' => __( 'Inactive', 'stream-notifications' ),
+ 'url' => admin_url( sprintf( 'admin.php?page=%s&visibility=inactive', WP_Stream_Notifications::NOTIFICATIONS_PAGE_SLUG ) ),
+ 'link_class' => null,
+ 'li_class' => null,
+ 'count' => $this->count_records( array( 'visibility' => 'inactive' ) ),
+ ),
+ );
+
+ $navigation_items = apply_filters( 'wp_stream_notifications_list_navigation_array', $navigation_items );
+
+ $navigation_links = array();
+ $navigation_html = '';
+ $visibility = filter_input( INPUT_GET, 'visibility', FILTER_DEFAULT, array( 'options' => array( 'default' => 'all' ) ) );
+
+ $i = 0;
+
+ foreach ( $navigation_items as $visibility_filter => $item ) {
+ $i++;
+ $navigation_links[] = sprintf(
+ '%s%s%s',
+ esc_attr( $item[ 'li_class' ] ),
+ esc_attr( $item[ 'url' ] ),
+ $visibility == $visibility_filter
+ ? 'current ' . esc_attr( $item[ 'link_class' ] )
+ : esc_attr( $item[ 'link_class' ] ),
+ esc_html( $item[ 'link_text' ] ),
+ $item[ 'count' ] !== null
+ ? sprintf( ' (%s)', esc_html( $item[ 'count' ] ) )
+ : '',
+ $i === count( $navigation_items ) ? '' : ' | '
+ );
+ }
+
+ $navigation_links = apply_filters( 'wp_stream_notifications_list_navigation_links', $navigation_links );
+ $navigation_html = is_array( $navigation_links ) ? implode( "\n", $navigation_links ) : $navigation_links;
+
+ $out = sprintf(
+ '',
+ $navigation_html
+ );
+
+ return apply_filters( 'wp_stream_notifications_list_navigation_html', $out );
+ }
+
+ function filter_search() {
+ $out = sprintf(
+ '
+
+
+
+
',
+ esc_attr__( 'Search Notifications', 'stream-notifications' ),
+ isset( $_GET['search'] ) ? esc_attr( $_GET['search'] ) : null
+ );
+
+ return $out;
+ }
+
+ function display() {
+ echo $this->list_navigation(); // xss ok
+ echo '';
+ }
+
+ function display_tablenav( $which ) {
+ if ( 'top' == $which ) { ?>
+
+ extra_tablenav( $which );
+ $this->pagination( $which );
+ ?>
+
+
+
+
+
+ extra_tablenav( $which );
+ $this->pagination( $which );
+ ?>
+
+
+
+ visibility;
+
+ $row_class = sprintf( 'class="%s"', implode( ' ', $row_classes ) );
+
+ echo sprintf( '', $row_class ); // xss ok
+ $this->single_row_columns( $item );
+ echo '
';
+ }
+
+ static function set_screen_option( $dummy, $option, $value ) {
+ if ( $option == 'edit_stream_notifications_per_page' ) {
+ return $value;
+ } else {
+ return $dummy;
+ }
+ }
+
+ function get_rule_types( $item ) {
+ $rule = get_option( sprintf( 'stream_notifications_%d', $item->ID ) );
+ if ( empty( $rule['alerts'] ) ) {
+ return __( 'N/A', 'stream_notification' );
+ }
+ $types = wp_list_pluck( $rule['alerts'], 'type' );
+ $titles = wp_list_pluck(
+ array_intersect_key(
+ WP_Stream_Notifications::$adapters,
+ array_flip( $types )
+ ),
+ 'title'
+ );
+ return implode( ', ', $titles );
+ }
+
+}
diff --git a/stream-notifications.php b/stream-notifications.php
index 171759adc..5c57e2465 100644
--- a/stream-notifications.php
+++ b/stream-notifications.php
@@ -8,7 +8,7 @@
* Author: X-Team
* Author URI: http://x-team.com/wordpress/
* License: GPLv2+
- * Text Domain: stream
+ * Text Domain: stream-notifications
* Domain Path: /languages
*/
@@ -54,6 +54,16 @@ class WP_Stream_Notifications {
*/
public static $screen_id;
+ /**
+ * List table object
+ * @var WP_Stream_Notifications_List_Table
+ */
+ public static $list_table = null;
+
+ const NOTIFICATIONS_PAGE_SLUG = 'wp_stream_notifications';
+ // Todo: We should probably check whether the current user has caps to
+ // view and edit the notifications as this can differ from caps to Stream.
+
/**
* Holds admin notices messages
*
@@ -67,6 +77,12 @@ class WP_Stream_Notifications {
*/
public static $adapters = array();
+ /**
+ * Matcher object
+ * @var WP_Stream_Notification_Rule_Matcher
+ */
+ public $matcher;
+
/**
* Class constructor
*/
@@ -105,8 +121,16 @@ public function load() {
add_action( 'admin_menu', array( $this, 'register_menu' ), 11 );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ), 11 );
+ // Default list actions handlers
+ add_action( 'wp_stream_notifications_handle_deactivate', array( __CLASS__, 'handle_rule_activation_status_change' ), 10, 3 );
+ add_action( 'wp_stream_notifications_handle_activate', array( __CLASS__, 'handle_rule_activation_status_change' ), 10, 3 );
+ add_action( 'wp_stream_notifications_handle_delete', array( __CLASS__, 'handle_rule_deletion' ), 10, 3 );
+
// AJAX end point for form auto completion
add_action( 'wp_ajax_stream_notification_endpoint', array( $this, 'form_ajax_ep' ) );
+
+ // Load Matcher
+ $this->matcher = new WP_Stream_Notification_Rule_Matcher();
}
/**
@@ -118,10 +142,10 @@ public function load() {
public function register_menu() {
self::$screen_id = add_submenu_page(
'wp_stream',
- __( 'Notifications', 'stream' ),
- __( 'Notifications', 'stream' ),
+ __( 'Notifications', 'stream-notifications' ),
+ __( 'Notifications', 'stream-notifications' ),
'manage_options',
- 'wp_stream_notifications',
+ self::NOTIFICATIONS_PAGE_SLUG,
array( $this, 'page' )
);
@@ -140,11 +164,15 @@ public function enqueue_scripts( $hook ) {
return;
}
- wp_enqueue_style( 'select2' );
- wp_enqueue_script( 'select2' );
- wp_enqueue_script( 'underscore' );
- wp_enqueue_script( 'stream-notifications-main', WP_STREAM_NOTIFICATIONS_URL . '/ui/js/main.js', array( 'underscore', 'select2' ) );
- wp_localize_script( 'stream-notifications-main', 'stream_notifications', $this->get_js_options() );
+ $view = filter_input( INPUT_GET, 'view', FILTER_DEFAULT, array( 'options' => array( 'default' => 'list' ) ) );
+
+ if ( $view == 'rule' ) {
+ wp_enqueue_style( 'select2' );
+ wp_enqueue_script( 'select2' );
+ wp_enqueue_script( 'underscore' );
+ wp_enqueue_script( 'stream-notifications-main', WP_STREAM_NOTIFICATIONS_URL . '/ui/js/main.js', array( 'underscore', 'select2' ) );
+ wp_localize_script( 'stream-notifications-main', 'stream_notifications', $this->get_js_options() );
+ }
}
public static function register_adapter( $adapter, $name, $title ) {
@@ -168,93 +196,92 @@ public function get_js_options() {
$args['types'] = array(
'search' => array(
- 'title' => __( 'Summary', 'stream' ),
+ 'title' => __( 'Summary', 'stream-notifications' ),
'type' => 'text',
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'contains' => __( 'contains', 'stream' ),
- 'contains-not' => __( 'does not contain', 'stream' ),
- 'regex' => __( 'regex', 'stream' ),
- ),
- ),
- 'object_type' => array(
- 'title' => __( 'Object Type', 'stream' ),
- 'type' => 'select',
- 'multiple' => true,
- 'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- 'not_in' => __( 'not in', 'stream' ),
- ),
- 'options' => array( // TODO: Do we have a dynamic way to get this ?
- 'user' => __( 'User', 'stream' ),
- 'post' => __( 'Post', 'stream' ),
- 'comment' => __( 'Comment', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'contains' => __( 'contains', 'stream-notifications' ),
+ '!contains' => __( 'does not contain', 'stream-notifications' ),
+ 'regex' => __( 'regex', 'stream-notifications' ),
),
),
+ // 'object_type' => array(
+ // 'title' => __( 'Object Type', 'stream-notifications' ),
+ // 'type' => 'select',
+ // 'multiple' => true,
+ // 'operators' => array(
+ // '=' => __( 'is', 'stream-notifications' ),
+ // '!=' => __( 'is not', 'stream-notifications' ),
+ // 'in' => __( 'in', 'stream-notifications' ),
+ // 'not_in' => __( 'not in', 'stream-notifications' ),
+ // ),
+ // 'options' => array( // TODO: Do we have a dynamic way to get this ?: Answer: NO, use 'Context'
+ // 'user' => __( 'User', 'stream-notifications' ),
+ // 'post' => __( 'Post', 'stream-notifications' ),
+ // 'comment' => __( 'Comment', 'stream-notifications' ),
+ // ),
+ // ),
- // TODO: Show object title in front end if both object type / id are set
'object_id' => array(
- 'title' => __( 'Object ID', 'stream' ),
+ 'title' => __( 'Object ID', 'stream-notifications' ),
'type' => 'text',
'tags' => true,
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- 'not_in' => __( 'not in', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'in' => __( 'in', 'stream-notifications' ),
+ 'not_in' => __( 'not in', 'stream-notifications' ),
),
),
'author_role' => array(
- 'title' => __( 'Author Role', 'stream' ),
+ 'title' => __( 'Author Role', 'stream-notifications' ),
'type' => 'select',
'multiple' => true,
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- '!in' => __( 'not in', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'in' => __( 'in', 'stream-notifications' ),
+ '!in' => __( 'not in', 'stream-notifications' ),
),
'options' => $roles_arr,
),
'author' => array(
- 'title' => __( 'Author', 'stream' ),
+ 'title' => __( 'Author', 'stream-notifications' ),
'type' => 'text',
'ajax' => true,
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- '!in' => __( 'not in', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'in' => __( 'in', 'stream-notifications' ),
+ '!in' => __( 'not in', 'stream-notifications' ),
),
),
'ip' => array(
- 'title' => __( 'IP', 'stream' ),
+ 'title' => __( 'IP', 'stream-notifications' ),
'type' => 'text',
'tags' => true,
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- '!in' => __( 'not in', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'in' => __( 'in', 'stream-notifications' ),
+ '!in' => __( 'not in', 'stream-notifications' ),
),
),
'date' => array(
- 'title' => __( 'Date', 'stream' ),
+ 'title' => __( 'Date', 'stream-notifications' ),
'type' => 'date',
'operators' => array(
- '=' => __( 'is on', 'stream' ),
- '!=' => __( 'is not on', 'stream' ),
- '<' => __( 'is before', 'stream' ),
- '<=' => __( 'is on or before', 'stream' ),
- '>' => __( 'is after', 'stream' ),
- '>=' => __( 'is on or after', 'stream' ),
+ '=' => __( 'is on', 'stream-notifications' ),
+ '!=' => __( 'is not on', 'stream-notifications' ),
+ '<' => __( 'is before', 'stream-notifications' ),
+ '<=' => __( 'is on or before', 'stream-notifications' ),
+ '>' => __( 'is after', 'stream-notifications' ),
+ '>=' => __( 'is on or after', 'stream-notifications' ),
),
),
@@ -263,40 +290,40 @@ public function get_js_options() {
// 'meta_query' => array(),
'connector' => array(
- 'title' => __( 'Connector', 'stream' ),
+ 'title' => __( 'Connector', 'stream-notifications' ),
'type' => 'select',
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- '!in' => __( 'not in', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'in' => __( 'in', 'stream-notifications' ),
+ '!in' => __( 'not in', 'stream-notifications' ),
),
'options' => WP_Stream_Connectors::$term_labels['stream_connector'],
),
'context' => array(
- 'title' => __( 'Context', 'stream' ),
+ 'title' => __( 'Context', 'stream-notifications' ),
'type' => 'text',
'ajax' => true,
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- '!in' => __( 'not in', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'in' => __( 'in', 'stream-notifications' ),
+ '!in' => __( 'not in', 'stream-notifications' ),
),
),
'action' => array(
- 'title' => __( 'Action', 'stream' ),
+ 'title' => __( 'Action', 'stream-notifications' ),
'type' => 'text',
'ajax' => true,
'operators' => array(
- '=' => __( 'is', 'stream' ),
- '!=' => __( 'is not', 'stream' ),
- 'in' => __( 'in', 'stream' ),
- '!in' => __( 'not in', 'stream' ),
+ '=' => __( 'is', 'stream-notifications' ),
+ '!=' => __( 'is not', 'stream-notifications' ),
+ 'in' => __( 'in', 'stream-notifications' ),
+ '!in' => __( 'not in', 'stream-notifications' ),
),
),
);
-
+
$args['adapters'] = array();
foreach ( self::$adapters as $name => $options ) {
@@ -311,19 +338,18 @@ public function get_js_options() {
/**
* Admin page callback function, redirects to each respective method based
- * on $_GET['action']
+ * on $_GET['view']
*
* @return void
*/
public function page() {
- $action = filter_input( INPUT_GET, 'action', FILTER_DEFAULT, array( 'default' => 'list' ) );
- $id = filter_input( INPUT_GET, 'id', FILTER_DEFAULT );
- switch ( $action ) {
- case 'add':
- case 'edit':
+ $view = filter_input( INPUT_GET, 'view', FILTER_DEFAULT, array( 'options' => array( 'default' => 'list' ) ) );
+ $id = filter_input( INPUT_GET, 'id' );
+
+ switch ( $view ) {
+ case 'rule':
$this->page_form( $id );
break;
- case 'list':
default:
$this->page_list();
break;
@@ -343,35 +369,84 @@ public function page_form( $id = null ) {
}
public function page_form_save() {
- // TODO add nonce, check author/user permission to update record
- // TODO Do not save if no triggers are added
- $action = filter_input( INPUT_GET, 'action' );
- $id = filter_input( INPUT_GET, 'id' );
+ require_once WP_STREAM_NOTIFICATIONS_INC_DIR . 'list-table.php';
+ self::$list_table = new WP_Stream_Notifications_List_Table( array( 'screen' => self::$screen_id ) );
- $rule = new WP_Stream_Notification_Rule( $id );
+ // TODO check author/user permission to update record
+
+ $view = filter_input( INPUT_GET, 'view', FILTER_DEFAULT, array( 'options' => array( 'default' => 'list' ) ) );
+ $action = filter_input( INPUT_GET, 'action', FILTER_DEFAULT );
+ $id = filter_input( INPUT_GET, 'id' );
+ $bulk_ids = filter_input( INPUT_GET, 'wp_stream_notifications_checkbox', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
+
+ // There is a chance we go from the bottom bulk actions select box
+ if ( ! $action || $action == '-1' ) {
+ $action = filter_input( INPUT_GET, 'action2', FILTER_DEFAULT, array( 'options' => array( 'default' => 'render' ) ) );
+ }
+
+ if ( $_POST && 'rule' == $view ) {
+ $data = $_POST;
+ $rule = new WP_Stream_Notification_Rule( $id );
- $data = $_POST;
+ if ( ! wp_verify_nonce( filter_input( INPUT_POST, '_wpnonce' ), 'stream-notifications-form' ) ) {
+ wp_die( __( 'Invalid form parameters.', 'stream-notifications' ) );
+ }
- if ( $data && in_array( $action, array( 'edit', 'add' ) ) ) {
+ if ( empty( $data['triggers'] ) ) {
+ wp_die( __( 'Rules cannot be saved without triggers!', 'stream-notifications' ) );
+ }
- if ( ! isset( $data['visibility'] ) ) $data['visibility'] = 0; // Checkbox woraround
+ if ( ! isset( $data['visibility'] ) ) {
+ $data['visibility'] = 'inactive'; // Checkbox woraround
+ }
$result = $rule->load_from_array( $data )->save();
+ if ( $result ) {
+ // Should not follow the WP naming convention, to avoid conflicts
+ // if/when Stream migrates to using WP tables
+ do_action( 'saved_stream_notification_rule', $rule );
+ }
+
if ( $result && $action != 'edit' ) {
wp_redirect( add_query_arg( array( 'action' => 'edit', 'id' => $rule->ID ) ) );
}
}
+
+ if ( 'list' == $view && 'render' != $action ) {
+ if ( has_action( 'wp_stream_notifications_handle_' . $action ) ) {
+ if ( $bulk_ids ) {
+ foreach ( $bulk_ids as $id ) {
+ do_action( 'wp_stream_notifications_handle_' . $action, $id, $action, true );
+ }
+ } else {
+ do_action( 'wp_stream_notifications_handle_' . $action, $id, $action, false );
+ }
+ } else {
+ wp_redirect( admin_url( 'admin.php?page=' . WP_Stream_Notifications::NOTIFICATIONS_PAGE_SLUG ) );
+ }
+ }
+
}
/**
- * Admin page callback for list action
+ * Admin page callback for list view
*
* @return void
*/
public function page_list() {
- // DEBUG, no listing yet
- ?>prepare_items();
+
+ echo '';
+ echo sprintf(
+ '
',
+ __( 'Stream Notifications', 'stream-notifications' ),
+ admin_url( 'admin.php?page=wp_stream_notifications&view=rule' ),
+ __( 'Add New' )
+ ); // xss okay
+
+ self::$list_table->display();
+ echo '
';
}
/**
@@ -382,18 +457,37 @@ public function page_list() {
*/
public function form_ajax_ep() {
// BIG @TODO: Make the request context-aware,
- // ie: get other rules ( maybe in the same group only ? ), so an author
- // query would check if there is a author_role rule available to limit
+ // ie: get other rules ( maybe in the same group only ? ), so an author
+ // query would check if there is a author_role rule available to limit
// the results according to it
- $type = filter_input( INPUT_POST, 'type' );
+
+ $type = filter_input( INPUT_POST, 'type' );
$is_single = filter_input( INPUT_POST, 'single' );
- $query = filter_input( INPUT_POST, 'q' );
+ $query = filter_input( INPUT_POST, 'q' );
if ( $is_single ) {
switch ( $type ) {
case 'author':
- $user = get_userdata( $query );
- $data = array( 'id' => $user->ID, 'text' => $user->display_name );
+ $user_ids = explode( ',', $query );
+ $user_query = new WP_User_Query(
+ array(
+ 'include' => $user_ids,
+ 'fields' => array( 'ID', 'user_email', 'display_name' )
+ )
+ );
+ if ( $user_query->results ) {
+ $data = $this->format_json_for_select2(
+ $user_query->results,
+ 'ID',
+ 'display_name'
+ );
+ } else {
+ $data = array();
+ }
+ break;
+ case 'action':
+ $actions = WP_Stream_Connectors::$term_labels['stream_action'];
+ $data = $this->format_json_for_select2( array( $query => $actions[$query] ) );
break;
}
} else {
@@ -405,7 +499,7 @@ public function form_ajax_ep() {
case 'action':
$actions = WP_Stream_Connectors::$term_labels['stream_action'];
$actions = preg_grep( sprintf( '/%s/i', $query ), $actions );
- $data = $this->format_json_for_select2( $actions );
+ $data = $this->format_json_for_select2( $actions );
break;
}
}
@@ -441,6 +535,87 @@ public function format_json_for_select2( $data, $key = null, $val = null ) {
return $return;
}
+ /*
+ * Handle the rule activation & deactivation action
+ */
+ public static function handle_rule_activation_status_change( $id, $action, $is_bulk = false ) {
+ $data = $_GET;
+ $nonce = filter_input( INPUT_GET, 'wp_stream_nonce' );
+ $nonce_identifier = $is_bulk ? 'wp_stream_notifications_bulk_actions' : "activate-record_$id";
+ $visibility = $action == 'activate' ? 'active' : 'inactive';
+
+ if ( ! wp_verify_nonce( $nonce, $nonce_identifier ) ) {
+ return;
+ }
+
+ $activate_rule = apply_filters( 'wp_stream_notifications_before_rule_' . $action, true, $id );
+ if ( $activate_rule == false ) {
+ return;
+ }
+
+ self::update_record(
+ $id,
+ array( 'visibility' => $visibility ),
+ array( '%s' )
+ );
+ wp_redirect( add_query_arg( array(
+ 'wp_stream_nonce' => false,
+ 'action' => false,
+ 'id' => false,
+ 'visibility' => $visibility,
+ ) ) );
+ }
+
+ /*
+ * Handle the rule deletion
+ */
+ public static function handle_rule_deletion( $id, $action, $is_bulk = false ) {
+ $data = $_GET;
+ $nonce = filter_input( INPUT_GET, 'wp_stream_nonce' );
+ $nonce_identifier = $is_bulk ? 'wp_stream_notifications_bulk_actions' : "delete-record_$id";
+ $visibility = filter_input( INPUT_GET, 'visibility', FILTER_DEFAULT );
+
+ if ( ! wp_verify_nonce( $nonce, $nonce_identifier ) ) {
+ return;
+ }
+
+ $activate_rule = apply_filters( 'wp_stream_notifications_before_rule_' . $action, true, $id );
+ if ( $activate_rule == false ) {
+ return;
+ }
+
+ self::delete_record( $id );
+ wp_redirect( add_query_arg( array(
+ 'wp_stream_nonce' => false,
+ 'action' => false,
+ 'id' => false,
+ 'visibility' => $visibility,
+ ) ) );
+ }
+
+ public function update_record( $id, $fields, $formats ) {
+ global $wpdb;
+
+ $wpdb->update(
+ WP_Stream_DB::$table,
+ $fields,
+ array( 'ID' => $id, 'type' => 'notification_rule' ),
+ $formats,
+ array( '%d', '%s' )
+ ); // db call ok, cache ok
+ }
+
+ public function delete_record( $id ) {
+ global $wpdb;
+
+ $wpdb->delete(
+ WP_Stream_DB::$table,
+ array(
+ 'ID' => $id,
+ )
+ ); // db call ok, cache ok
+ }
+
/**
* Check if plugin dependencies are satisfied and add an admin notice if not
*
@@ -450,9 +625,9 @@ public function is_dependency_satisfied() {
$message = '';
if ( ! class_exists( 'WP_Stream' ) ) {
- $message .= sprintf( '%s
', __( 'Stream Notifications requires Stream plugin to be present and activated.', 'stream' ) );
+ $message .= sprintf( '%s
', __( 'Stream Notifications requires Stream plugin to be present and activated.', 'stream-notifications' ) );
} else if ( version_compare( WP_Stream::VERSION, self::STREAM_MIN_VERSION, '<' ) ) {
- $message .= sprintf( '%s
', sprintf( __( 'Stream Notifications requires Stream version %s or higher', 'stream' ), self::STREAM_MIN_VERSION ) );
+ $message .= sprintf( '%s
', sprintf( __( 'Stream Notifications requires Stream version %s or higher', 'stream-notifications' ), self::STREAM_MIN_VERSION ) );
}
if ( ! empty( $message ) ) {
@@ -460,7 +635,7 @@ public function is_dependency_satisfied() {
'',
$message,
sprintf(
- __( 'Please install Stream plugin version %s or higher for Stream Notifications to work properly.', 'stream' ),
+ __( 'Please install Stream plugin version %s or higher for Stream Notifications to work properly.', 'stream-notifications' ),
esc_url( 'http://wordpress.org/plugins/stream/' ),
self::STREAM_MIN_VERSION
)
diff --git a/ui/js/main.js b/ui/js/main.js
index fc62a966c..5d96f77d0 100644
--- a/ui/js/main.js
+++ b/ui/js/main.js
@@ -1,15 +1,15 @@
/* globals stream_notifications, ajaxurl, triggers */
jQuery(function($){
'use strict';
-
+
_.templateSettings.variable = 'vars';
var types = stream_notifications.types,
i,
-
+
divTriggers = $('#triggers'), // Trigger Playground
- divAlerts = $('#alerts'), // Alerts Playground
-
+ divAlerts = $('#alerts .inside'), // Alerts Playground
+
btns = {
add_trigger: '.add-trigger',
add_alert: '.add-alert',
@@ -27,25 +27,24 @@ jQuery(function($){
allowClear: true,
width: '160px'
},
-
+
selectify = function( elements, args ) {
args = args || {};
$.extend( args, select2_args );
$(elements).filter(':not(.select2-offscreen)').each( function() {
var $this = $(this),
- elementArgs = args,
+ elementArgs = jQuery.extend( {}, args ),
tORa = $this.closest('#alerts, #triggers').attr('id');
;
elementArgs.width = parseInt( $this.css('width'), 10 ) + 30;
-
if ( $this.hasClass('ajax') ) {
var type = '';
if ( ! ( type = $this.data( 'ajax-key' ) ) ) {
if ( tORa == 'triggers' ) {
- type = $this.parents('.form-row').first().find('select.trigger_type').val();
+ type = $this.parents('.form-row').first().find('select.trigger-type').val();
} else {
- type = $this.parents('.form-row').eq(1).find('select.alert_type').val();
+ type = $this.parents('.form-row').eq(1).find('select.alert-type').val();
}
}
elementArgs.minimumInputLength = 3;
@@ -70,23 +69,44 @@ jQuery(function($){
};
elementArgs.initSelection = function(element, callback) {
var id = $(element).val();
- if ( id !== '' ) {
- $.ajax({
- url: ajaxurl,
- type: 'post',
- data: {
- action: 'stream_notification_endpoint',
- q : id,
- single: 1,
- type : type,
- },
- dataType: "json"
- }).done( function( data ) { callback( data.data ); } );
- }
+ if ( id !== '' ) {
+ $.ajax({
+ url: ajaxurl,
+ type: 'post',
+ data: {
+ action: 'stream_notification_endpoint',
+ q : id,
+ single: 1,
+ type : type,
+ },
+ dataType: "json"
+ }).done( function( data ) { callback( data.data ); } );
+ }
};
}
$this.select2( elementArgs );
+ $this.on( 'select2_populate', function( e, val ) {
+ var $this = $(this);
+ if ( $this.hasClass('ajax') ) {
+ $.ajax({
+ url: ajaxurl,
+ type: 'post',
+ data: {
+ action: 'stream_notification_endpoint',
+ q : val,
+ single: 1,
+ type : type,
+ },
+ dataType: "json",
+ success: function(j){
+ $this.select2( 'data', j.data );
+ }
+ })
+ } else {
+ $this.select2( 'data', [{ id: val, text: val }] );
+ }
+ } );
});
};
@@ -94,10 +114,10 @@ jQuery(function($){
// Add new rule
.on( 'click.sn', btns.add_trigger, function(e) {
e.preventDefault();
- var $this = $(this),
- index = 0,
- lastItem = null,
- group = divTriggers.find('.group').filter( '[rel=' + $this.data('group') + ']' );
+ var $this = $(this),
+ index = 0,
+ lastItem = null,
+ group = divTriggers.find('.group').filter( '[rel=' + $this.data('group') + ']' );
if ( ( lastItem = divTriggers.find('.trigger').last() ) && lastItem.size() ) {
index = parseInt( lastItem.attr('rel') ) + 1;
@@ -106,11 +126,11 @@ jQuery(function($){
group.append( tmpl( $.extend(
{ index: index, group: $this.data('group') },
stream_notifications
- ) ) );
+ ) ) );
group.find('.trigger').first().addClass('first');
selectify( group.find('select') );
})
-
+
// Add new group
.on( 'click.sn', btns.add_group, function(e, groupIndex) {
e.preventDefault();
@@ -145,14 +165,14 @@ jQuery(function($){
})
// Reveal rule options after choosing rule type
- .on( 'change.sn', '.trigger_type', function() {
+ .on( 'change.sn', '.trigger-type', function() {
var $this = $(this),
options = types[ $this.val() ],
index = $this.parents('.trigger').first().attr('rel');
- $this.next('.trigger_options').remove();
-
+ $this.next('.trigger-options').remove();
+
if ( ! options ) { return; }
-
+
$this.after( tmpl_options( $.extend( options, { index: index } ) ) );
selectify( $this.parent().find('select') );
selectify( $this.parent().find('input.tags, input.ajax'), { tags: [] } );
@@ -169,16 +189,16 @@ jQuery(function($){
divAlerts.append( tmpl_alert( $.extend(
{ index: index },
stream_notifications
- ) ) );
+ ) ) );
selectify( divAlerts.find('.alert select') );
})
// Reveal rule options after choosing rule type
- .on( 'change.sn', '.alert_type', function() {
+ .on( 'change.sn', '.alert-type', function() {
var $this = $(this),
options = stream_notifications.adapters[ $this.val() ],
index = $this.parents('.alert').first().attr('rel');
- $this.next('.alert_options').remove();
+ $this.next('.alert-options').remove();
if ( ! options ) { return; }
@@ -186,6 +206,14 @@ jQuery(function($){
selectify( $this.parent().find('select') );
selectify( $this.parent().find('input.tags, input.ajax'), { tags: [] } );
})
+
+ // Delete an alert
+ .on( 'click.sn', '.delete-alert', function(e) {
+ e.preventDefault();
+ var $this = $(this);
+
+ $this.parents('.alert').first().remove();
+ })
;
// Populate form values if it exists
@@ -202,7 +230,7 @@ jQuery(function($){
var group = notification_rule.groups[trigger.group];
$( btns.add_group ).filter('[data-group='+group.group+']').trigger('click', trigger.group);
groupDiv = divTriggers.find('.group').filter('[rel='+trigger.group+']');
- groupDiv.find('select.group_relation').select2( 'val', group.relation );
+ groupDiv.find('select.group-relation').select2( 'val', group.relation );
}
// create the new row, by clicking the add-trigger button in the appropriate group
@@ -211,15 +239,16 @@ jQuery(function($){
// populate values
row = groupDiv.find('.trigger:last');
- row.find('select.trigger_relation').select2( 'val', trigger.relation ).trigger('change');
- row.find('select.trigger_type').select2( 'val', trigger.type ).trigger('change');
- row.find('select.trigger_operator').select2( 'val', trigger.operator ).trigger('change');
+ row.find('select.trigger-relation').select2( 'val', trigger.relation ).trigger('change');
+ row.find('select.trigger-type').select2( 'val', trigger.type ).trigger('change');
+ row.find('select.trigger-operator').select2( 'val', trigger.operator ).trigger('change');
// populate the trigger value, according to the trigger type
if ( trigger.value ) {
valueField = row.find('.trigger_value:not(.select2-container)').eq(0);
if ( valueField.is('select') || valueField.is('.ajax') ) {
- valueField.select2( 'val', trigger.value ).trigger('change');
+ valueField.trigger( 'select2_populate', trigger.value );
+ // valueField.select2( 'val', trigger.value ).trigger('change');
} else {
valueField.val( trigger.value ).trigger('change');
}
@@ -238,8 +267,8 @@ jQuery(function($){
// populate values
row = divAlerts.find('.alert:last');
- row.find('select.alert_type').select2( 'val', alert.type ).trigger('change');
- optionFields = row.find('.alert_options');
+ row.find('select.alert-type').select2( 'val', alert.type ).trigger('change');
+ optionFields = row.find('.alert-options');
optionFields.find(':input[name]').each(function(i, el){
var $this = $(this),
name,
@@ -247,7 +276,12 @@ jQuery(function($){
name = $this.attr('name').match('\\[([a-z_\-]+)\\]$')[1];
if ( typeof alert[name] != 'undefined' ) {
val = alert[name];
- $this.val( val ).trigger('change');
+ if ( $this.hasClass( 'select2-offscreen' ) ) {
+ $this.trigger( 'select2_populate', val )
+ // $this.select2( 'val', val ).trigger( 'change' );
+ } else {
+ $this.val( val ).trigger('change');
+ }
}
});
}
diff --git a/views/rule-form.php b/views/rule-form.php
index 71907cc78..9b977dcf5 100644
--- a/views/rule-form.php
+++ b/views/rule-form.php
@@ -1,15 +1,18 @@
-
exists() ? _e( 'Edit Notification Rule', 'stream_notification' ) : _e( 'Add Notification Rule', 'stream_notification' ); ?>
+
exists() ? _e( 'Edit Notification Rule', 'stream-notifications' ) : _e( 'Add Notification Rule', 'stream-notifications' ); ?>