Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic notification / action data #88

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
834d9b9
Allow action.data to be more flexible, with a callback and a sanitisa…
shadyvb Jul 5, 2019
87fbf8d
Add a filter for notification objects pre-output
shadyvb Jul 5, 2019
5fe34ca
Update changelog and package version
shadyvb Jul 5, 2019
3ec58ce
Add use statement for the $user variable
shadyvb Jul 5, 2019
f5fd438
Add data to notification
mattheu Jul 8, 2019
a67c568
Use microtime not time
mattheu Jul 9, 2019
ac13a8a
Better data prop schema
mattheu Jul 9, 2019
b8b1002
Add data to notification (#90)
shadyvb Jul 9, 2019
2772f52
Update notification data handling
shadyvb Jul 9, 2019
9c806fe
Add event id as type param in messages
shadyvb Jul 9, 2019
a375b28
Pump version in plugin.php
shadyvb Jul 9, 2019
c9cb442
Enhance data sanitisation and filtering for notifications and actions
shadyvb Jul 9, 2019
2392b1e
Fix missing return statement
shadyvb Jul 9, 2019
1dbd1eb
Allow filtering allowed HTML tags in notification subject
shadyvb Jul 10, 2019
acca009
Refine allowed tags filter name
shadyvb Jul 10, 2019
313a8cb
Add a method to remove message actions
shadyvb Jul 10, 2019
41ed53d
Add filter to notifications output and filter out empty notification …
shadyvb Jul 11, 2019
d49b39d
Fix filter name
shadyvb Jul 11, 2019
94915c1
Fix notice
shadyvb Jul 12, 2019
252deea
Merge remote-tracking branch 'origin/master' into dynamic-action-data
mattheu Jul 29, 2022
8cebdac
Remove bump to changelog version
mattheu Jul 29, 2022
a1ee7ad
Merge branch 'master' of github.com:humanmade/Workflows into dynamic-…
mattheu Oct 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Changelog
=========

- Enhancement: Allow action data attribute to be a callback receiving action arguments and returning data to be stored, to allow dynamic on-execution data storage.
- Enhancement: Add a new filter to allow manipulating notification object before output, allows for further highly-dynamic data gathering on-output, or data that shouldn't be stored in DB.
- Enhancement: Allow overriding the action data sanitisation callback to permit more flexible schemas.

v0.4.6
- Bug: Do not discard previous filter value when excluding editorial comments from post comment count.

Expand Down
50 changes: 47 additions & 3 deletions inc/class-event.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ class Event {
*/
protected $message_actions = [];

/**
* Event data callback
*
* @var callable
*/
protected $message_data_callback = null;

/**
* Event recipient handlers.
*
Expand Down Expand Up @@ -167,6 +174,19 @@ public function add_message_tags( array $tags ): Event {
return $this;
}

/**
* Add optional data callback to execute on the event execution
*
* @param callable $callback
*
* @return \HM\Workflows\Event
*/
public function add_message_data_callback( callable $callback ): Event {
$this->message_data_callback = $callback;

return $this;
}

/**
* Adds a message action.
*
Expand All @@ -177,12 +197,12 @@ public function add_message_tags( array $tags ): Event {
* @param null|callable $args An optional function that receives the return value of the event action.
* @param array $schema An array of accepted $_GET arguments and their corresponding
* sanitisation callbacks.
* @param array $data Arbitrary data passed to the Destination via $messages to any other
* action data such as type.
* @param array|callable $data Arbitrary data passed to the Destination via $messages to any other
* action data such as type. Pass a callback to execute with action args.
*
* @return Event
*/
public function add_message_action( string $id, string $text, $callback_or_url, $args = null, array $schema = [], array $data = [] ): Event {
public function add_message_action( string $id, string $text, $callback_or_url, $args = null, array $schema = [], $data = [] ): Event {
$this->message_actions[ $id ] = [
'text' => $text,
'callback_or_url' => $callback_or_url,
Expand All @@ -194,6 +214,21 @@ public function add_message_action( string $id, string $text, $callback_or_url,
return $this;
}

/**
* Remove a message action.
*
* @param string $id The reference name for the action.
*
* @return Event
*/
public function remove_message_action( string $id ) {
if ( isset( $this->message_actions[ $id ] ) ) {
unset( $this->message_actions[ $id ] );
}

return $this;
}

/**
* This method should add the $name and $callback parameter to $this->recipient_handlers with $id as the key.
*
Expand Down Expand Up @@ -301,6 +336,15 @@ public function get_message_action( string $id ): array {
return $this->message_actions[ $id ] ?? null;
}

/**
* Gets the message data callback
*
* @return callable|null
*/
public function get_message_data_callback(): ? callable {
return $this->message_data_callback;
}

/**
* Gets the Event UI object.
*
Expand Down
16 changes: 15 additions & 1 deletion inc/class-workflow.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,18 @@ public function run( array $args = [] ) {
}

$parsed_message = [];
$parsed_message['type'] = $this->event->get_id();
$parsed_message['subject'] = str_replace( array_keys( $tags ), array_values( $tags ), $subject );
$parsed_message['text'] = str_replace( array_keys( $tags ), array_values( $tags ), $text );
$parsed_message['time'] = microtime( true );
$parsed_message['data'] = [];
$parsed_message['actions'] = [];

$data_callback = $this->event->get_message_data_callback();
if ( is_callable( $data_callback ) ) {
$parsed_message['data'] = call_user_func_array( $data_callback, $args );
}

// Add actions from the message if any.
foreach ( $message['actions'] as $id => $action ) {
$this->event->add_message_action(
Expand Down Expand Up @@ -407,10 +415,16 @@ public function run( array $args = [] ) {
continue;
}

if ( is_callable( $action['data'] ) ) {
$data = call_user_func_array( $action['data'], $args );
} else {
$data = $action['data'];
}

$parsed_message['actions'][ $id ] = [
'text' => $action['text'],
'url' => $url,
'data' => $action['data'],
'data' => $data,
];
}

Expand Down
142 changes: 128 additions & 14 deletions lib/destinations/dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
add_action( 'rest_api_init', function () {

$schema = [
'type' => [
'type' => 'string',
'required' => true,
],
'subject' => [
'type' => 'string',
'required' => true,
Expand All @@ -30,6 +34,13 @@
'type' => 'string',
'required' => true,
],
'time' => [
'type' => 'int',
'required' => true,
],
'data' => [
'type' => 'object',
],
'actions' => [
'type' => 'array',
'items' => [
Expand Down Expand Up @@ -117,16 +128,32 @@ function get_notifications( WP_User $user ) {
return [];
}

$notifications = array_map( function ( $notification ) {
$notifications = array_map( function ( $notification ) use ( $user ) {
// Decode the notification.
$notification = _decode( $notification );

/**
* Filters notification content for any dynamic changes that might be needed
*
* @param \WP_User $user User object
*
* @return object $notification
*/
$notification = apply_filters( 'hm.workflows.notification.pre.output', $notification, $user );

return sanitize_notification( $notification );
}, $notifications );

$notifications = array_values( $notifications );
$notifications = array_filter( array_values( $notifications ) );

return $notifications;
/**
* Filter user notifications before rendering
*
* @param \WP_User User object
*
* @return array Filtered notifications array
*/
return apply_filters( 'hm.workflows.notifications', $notifications, $user );
}

/**
Expand Down Expand Up @@ -189,31 +216,113 @@ function _encode( array $notification ) : string {
*/
function sanitize_notification( $notification ) {
$notification = wp_parse_args( $notification, [
'type' => '',
'subject' => '',
'text' => '',
'actions' => [],
] );

/**
* Filter notification data array
*
* @param array Notification array
*
* @return array Notification data array
*/
$data = apply_filters(
'hm.workflows.notification.data',
sanitize_notification_data( (array) ( $notification['data'] ?? [] ), 'notification:' . $notification['type'] ),
$notification
);

/**
* Filter allowed HTML tags in subjects
*
* @return array Array of tags to be used with wp_kses
*/
$allowed_subject_tags = apply_filters( 'hm.workflows.destination.subject.allowed.tags', [] );

$sanitized_notification = [
'subject' => wp_kses( $notification['subject'] ?? '', [] ),
'type' => sanitize_text_field( $notification['type'] ?? '' ),
'subject' => wp_kses( $notification['subject'] ?? '', $allowed_subject_tags ),
'text' => wp_kses_post( $notification['text'] ?? '' ),
'actions' => array_values( array_map( function ( $action, $id ) {
return [
'id' => isset( $action['id'] ) ? sanitize_key( $action['id'] ) : sanitize_key( $id ),
'text' => sanitize_text_field( $action['text'] ),
'url' => esc_url_raw( $action['url'] ),
'data' => (object) array_map( 'sanitize_text_field', is_array( $action['data'] ) ? $action['data'] : [] ),
];
}, (array) $notification['actions'], array_keys( (array) $notification['actions'] ) ) ),
'time' => intval( $notification['time'] ?? 0 ),
'data' => (array) $data,
'actions' => [],
];

$sanitized_notification['actions'] = array_values( array_map( function ( $action, $id ) use ( $sanitized_notification ) {
$action_id = isset( $action['id'] ) ? sanitize_key( $action['id'] ) : sanitize_key( $id );

/**
* Filter notification action data
*
* @param array Action array
* @param array Notification array
*
* @return array Data array
*/
$data = apply_filters(
'hm.workflows.notification.action.data',
sanitize_notification_data( (array) $action['data'], 'action:' . $action_id ),
$action,
$sanitized_notification
);

return [
'id' => $action_id,
'text' => sanitize_text_field( $action['text'] ),
'url' => esc_url_raw( $action['url'] ),
'data' => (object) $data,
];
}, (array) $notification['actions'], array_keys( (array) $notification['actions'] ) ) );

if ( isset( $notification['id'] ) ) {
$sanitized_notification['id'] = intval( $notification['id'] );
}

return $sanitized_notification;
}

/**
* Sanitise notification or action data array, which may contains arbitrary data schema
*
* @param array $data Data array
* @param string $type Notification type or action ID, eg: 'notification:followed', 'action:follow'
* @param string|null $parent_key Parent key name if nested
*
* @return array
*/
function sanitize_notification_data( array $data, string $type, string $parent_key = null ) : array {
foreach ( $data as $key => $value ) {
$fullpath_key = ( $parent_key ? $parent_key . ':' : '' ) . $key;
/**
* Pre-filter data item value
*
* @param string $key Data item key, may contain parent key if present
* @param string $value Data item value
* @param string $type Notification type or action ID, eg: 'notification:followed', 'action:follow'
*
* @return mixed Data item value if pre-filtered, short-circuits the sanitization process
*/
$check = apply_filters( 'hm.workflows.notification.data.item', null, $fullpath_key, $value, $type );
if ( $check !== null ) {
$data[ $key ] = $check;
continue;
}

if ( is_numeric( $value ) ) {
$data[ $key ] = floatval( $value );
} elseif ( is_string( $value ) ) {
$data[ $key ] = sanitize_text_field( $value );
} elseif ( is_object( $value ) || is_array( $value ) ) {
$data[ $key ] = sanitize_notification_data( (array) $value, $type, $fullpath_key );
}
}

return (array) $data;
}

/**
* Return all notifications for a user.
*
Expand Down Expand Up @@ -271,9 +380,12 @@ function create( WP_REST_Request $request ) {
}

$notification = sanitize_notification( [
'actions' => $request->get_param( 'actions' ),
'data' => $request->get_param( 'data' ),
'subject' => $request->get_param( 'subject' ),
'text' => $request->get_param( 'text' ),
'actions' => $request->get_param( 'actions' ),
'time' => $request->get_param( 'time' ),
'type' => $request->get_param( 'type' ),
] );

// Store a placeholder to get a meta ID.
Expand Down Expand Up @@ -321,6 +433,8 @@ function edit( WP_REST_Request $request ) {
'id' => $old_notification['id'],
'subject' => $request->get_param( 'subject' ) ?: $old_notification['subject'],
'text' => $request->get_param( 'text' ) ?: $old_notification['text'],
'data' => $request->get_param( 'data' ) ?: $old_notification['data'],
'type' => $request->get_param( 'type' ) ?: $old_notification['type'],
'actions' => $request->get_param( 'actions' ) ?: $old_notification['actions'],
] );

Expand Down Expand Up @@ -364,7 +478,7 @@ function delete( WP_REST_Request $request ) {
}

clear_cache( $user->ID );
rest_ensure_response( $result );
return rest_ensure_response( $result );
}

/**
Expand Down