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 '
    '; + parent::display(); + 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( + '

    %s %s

    ', + __( '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() { '
    %s

    %s

    ', $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' ); ?>

    + + +
    - +
    @@ -19,7 +22,7 @@

    - +

    @@ -28,25 +31,36 @@
    - visibility, 1 ) ?>> + visibility, 'active' ) ?>>
    + exists() ): ?> +
    - +
    @@ -62,12 +76,12 @@

    - +

    - - + +
    @@ -77,10 +91,9 @@
    -

    +

    - - +
    @@ -92,32 +105,32 @@
    -triggers ): ?> - - + triggers ) { ?> + + \ No newline at end of file +