| 1 |
<?php |
| 2 |
/** |
| 3 |
* Ability: update fields of an existing notification. |
| 4 |
* |
| 5 |
* @package NotificationX\Abilities\Manage |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace NotificationX\Abilities\Manage; |
| 9 |
|
| 10 |
use NotificationX\Abilities\AbilityBase; |
| 11 |
use NotificationX\Abilities\BuilderInfo; |
| 12 |
use NotificationX\Core\PostType; |
| 13 |
|
| 14 |
if ( ! defined( 'ABSPATH' ) ) { |
| 15 |
exit; |
| 16 |
} |
| 17 |
|
| 18 |
/** |
| 19 |
* Updates the title, theme, data source and/or configuration fields of a |
| 20 |
* notification. |
| 21 |
* |
| 22 |
* The notification is loaded, the requested changes are merged over its stored |
| 23 |
* data, and it is saved back through the normal PostType::save_post() pipeline |
| 24 |
* so all NotificationX filters/hooks still run. The data source *can* be changed |
| 25 |
* (as in the admin); when it is, the notification type is realigned to the new |
| 26 |
* source automatically and the theme is validated/defaulted for that source. |
| 27 |
*/ |
| 28 |
class UpdateNotification extends AbilityBase { |
| 29 |
|
| 30 |
protected $id = 'notificationx/update-notification'; |
| 31 |
protected $label = 'Update a notification'; |
| 32 |
protected $description = 'Update an existing notification: change its title, theme, data source, or other configuration fields. Provide nx_id plus what to change. Changing "source" realigns the type automatically and, if you do not also pass a valid "themes", resets the theme to that source\'s default — call describe-type for the new source to get valid theme ids and content fields. Invalid theme/source values are rejected (not silently ignored).'; |
| 33 |
protected $is_write = true; |
| 34 |
protected $is_idempotent = true; |
| 35 |
|
| 36 |
/** |
| 37 |
* Fields that must never be overwritten via the free-form `fields` map. |
| 38 |
* (source/type are handled explicitly as first-class inputs.) |
| 39 |
* |
| 40 |
* @var string[] |
| 41 |
*/ |
| 42 |
protected $protected_keys = array( 'nx_id', 'type', 'source', 'created_at', 'update_status' ); |
| 43 |
|
| 44 |
public function input_schema() { |
| 45 |
return array( |
| 46 |
'type' => 'object', |
| 47 |
'required' => array( 'nx_id' ), |
| 48 |
'properties' => array( |
| 49 |
'nx_id' => array( |
| 50 |
'type' => 'integer', |
| 51 |
'description' => 'The notification id to update.', |
| 52 |
), |
| 53 |
'title' => array( |
| 54 |
'type' => 'string', |
| 55 |
'description' => 'New title for the notification.', |
| 56 |
), |
| 57 |
'source' => array( |
| 58 |
'type' => 'string', |
| 59 |
'description' => 'New data source id (from list-sources / describe-type). Changing it realigns the type and, unless a valid "themes" is also given, resets the theme to the new source default.', |
| 60 |
), |
| 61 |
'themes' => array( |
| 62 |
'type' => 'string', |
| 63 |
'description' => 'New theme id (must be valid for the notification\'s source; see describe-type). Rejected if invalid.', |
| 64 |
), |
| 65 |
'fields' => array( |
| 66 |
'type' => 'object', |
| 67 |
'description' => 'Advanced: a map of additional configuration fields to merge (e.g. content/design settings). Use get-notification / describe-type first to see the available fields.', |
| 68 |
), |
| 69 |
), |
| 70 |
); |
| 71 |
} |
| 72 |
|
| 73 |
public function output_schema() { |
| 74 |
return array( |
| 75 |
'type' => 'object', |
| 76 |
'properties' => array( |
| 77 |
'nx_id' => array( 'type' => 'integer' ), |
| 78 |
'applied' => array( 'type' => 'array' ), |
| 79 |
'ignored' => array( 'type' => 'array' ), |
| 80 |
'warnings' => array( 'type' => 'array' ), |
| 81 |
'notification' => array( 'type' => 'object' ), |
| 82 |
), |
| 83 |
); |
| 84 |
} |
| 85 |
|
| 86 |
public function execute( $input ) { |
| 87 |
$nx_id = (int) $input['nx_id']; |
| 88 |
$post_type = PostType::get_instance(); |
| 89 |
$existing = $post_type->get_post( $nx_id ); |
| 90 |
|
| 91 |
if ( empty( $existing ) ) { |
| 92 |
return new \WP_Error( |
| 93 |
'nx_mcp_not_found', |
| 94 |
/* translators: %d: notification id. */ |
| 95 |
sprintf( __( 'No notification found with id %d.', 'notificationx' ), $nx_id ), |
| 96 |
array( 'status' => 404 ) |
| 97 |
); |
| 98 |
} |
| 99 |
|
| 100 |
$applied = array(); |
| 101 |
$ignored = array(); |
| 102 |
$warnings = array(); |
| 103 |
|
| 104 |
// Start from the stored config so save_post() has everything it needs. |
| 105 |
$data = $existing; |
| 106 |
$old_source = isset( $existing['source'] ) ? $existing['source'] : ''; |
| 107 |
$source = $old_source; |
| 108 |
$source_changed = false; |
| 109 |
|
| 110 |
// --- Data source change (validated) --------------------------------- |
| 111 |
if ( ! empty( $input['source'] ) && (string) $input['source'] !== $old_source ) { |
| 112 |
$new_source = sanitize_text_field( $input['source'] ); |
| 113 |
if ( ! BuilderInfo::source_exists( $new_source ) ) { |
| 114 |
return new \WP_Error( |
| 115 |
'nx_mcp_invalid_source', |
| 116 |
/* translators: %s: source id. */ |
| 117 |
sprintf( __( 'Unknown data source "%s". Call list-sources for valid ids.', 'notificationx' ), $new_source ), |
| 118 |
array( 'status' => 400 ) |
| 119 |
); |
| 120 |
} |
| 121 |
if ( ! BuilderInfo::source_enabled( $new_source ) ) { |
| 122 |
return new \WP_Error( |
| 123 |
'nx_mcp_source_disabled', |
| 124 |
/* translators: %s: source id. */ |
| 125 |
sprintf( __( 'The module for data source "%s" is disabled; enable it before using it.', 'notificationx' ), $new_source ), |
| 126 |
array( 'status' => 409 ) |
| 127 |
); |
| 128 |
} |
| 129 |
$source = $new_source; |
| 130 |
$source_changed = true; |
| 131 |
$applied[] = 'source'; |
| 132 |
} |
| 133 |
|
| 134 |
// Type always follows the source (authoritative). |
| 135 |
$type = BuilderInfo::type_for_source( $source ); |
| 136 |
if ( '' === $type ) { |
| 137 |
$type = isset( $existing['type'] ) ? $existing['type'] : $type; |
| 138 |
} |
| 139 |
if ( $type !== ( isset( $existing['type'] ) ? $existing['type'] : '' ) ) { |
| 140 |
$applied[] = 'type'; |
| 141 |
} |
| 142 |
|
| 143 |
// --- Theme (validated against the effective source) ----------------- |
| 144 |
// Only reject when the source's themes can actually be enumerated; a |
| 145 |
// source whose themes cannot be introspected accepts the given theme. |
| 146 |
$valid_themes = BuilderInfo::theme_ids_for_source( $source ); |
| 147 |
$theme_changed = false; |
| 148 |
$old_theme = isset( $existing['themes'] ) ? $existing['themes'] : ''; |
| 149 |
if ( ! empty( $input['themes'] ) ) { |
| 150 |
$theme = sanitize_text_field( $input['themes'] ); |
| 151 |
if ( ! empty( $valid_themes ) && ! in_array( $theme, $valid_themes, true ) ) { |
| 152 |
return new \WP_Error( |
| 153 |
'nx_mcp_invalid_theme', |
| 154 |
sprintf( |
| 155 |
/* translators: 1: theme id, 2: source id, 3: comma-separated valid ids. */ |
| 156 |
__( 'Theme "%1$s" is not valid for source "%2$s". Valid themes: %3$s', 'notificationx' ), |
| 157 |
$theme, |
| 158 |
$source, |
| 159 |
implode( ', ', $valid_themes ) |
| 160 |
), |
| 161 |
array( 'status' => 400 ) |
| 162 |
); |
| 163 |
} |
| 164 |
$data['themes'] = $theme; |
| 165 |
$applied[] = 'themes'; |
| 166 |
$theme_changed = ( $theme !== $old_theme ); |
| 167 |
} elseif ( $source_changed && ! empty( $valid_themes ) && ! in_array( isset( $data['themes'] ) ? $data['themes'] : '', $valid_themes, true ) ) { |
| 168 |
// Old theme belongs to the old source; reset to the new source default. |
| 169 |
$data['themes'] = BuilderInfo::effective_default_theme( $source ); |
| 170 |
$applied[] = 'themes'; |
| 171 |
$theme_changed = true; |
| 172 |
$warnings[] = sprintf( |
| 173 |
/* translators: 1: theme id, 2: source id. */ |
| 174 |
__( 'Theme reset to "%1$s" (the default for the new source "%2$s"). Pass a valid "themes" to choose another; see describe-type.', 'notificationx' ), |
| 175 |
$data['themes'], |
| 176 |
$source |
| 177 |
); |
| 178 |
} |
| 179 |
|
| 180 |
// When the theme changed and the caller didn't explicitly set a content |
| 181 |
// template, re-apply the new theme's default notification-template — same |
| 182 |
// as selecting a theme in the admin builder — so data-driven types don't |
| 183 |
// render blank after a theme/source switch. Restricted to the cases that |
| 184 |
// actually break (no existing template, or a source change that |
| 185 |
// invalidates the old tags) so a same-type theme swap never clobbers a |
| 186 |
// user's customised content. Static-content themes have no template here. |
| 187 |
$template_supplied = ! empty( $input['fields']['notification-template'] ); |
| 188 |
$existing_template = isset( $existing['notification-template'] ) ? $existing['notification-template'] : array(); |
| 189 |
if ( $theme_changed && ! $template_supplied && ( empty( $existing_template ) || $source_changed ) ) { |
| 190 |
$default_template = BuilderInfo::default_template_for_theme( $data['themes'] ); |
| 191 |
if ( ! empty( $default_template ) ) { |
| 192 |
$data['notification-template'] = $default_template; |
| 193 |
$applied[] = 'notification-template'; |
| 194 |
} |
| 195 |
} |
| 196 |
|
| 197 |
if ( $source_changed ) { |
| 198 |
$warnings[] = __( 'Data source changed: content fields from the previous source were kept. Set the new source\'s content via "fields" (see describe-type) so the notification renders as intended.', 'notificationx' ); |
| 199 |
} |
| 200 |
|
| 201 |
// --- Title ---------------------------------------------------------- |
| 202 |
if ( isset( $input['title'] ) ) { |
| 203 |
$data['title'] = sanitize_text_field( $input['title'] ); |
| 204 |
$applied[] = 'title'; |
| 205 |
} |
| 206 |
|
| 207 |
// --- Free-form fields (protected keys reported, not silently dropped) - |
| 208 |
if ( ! empty( $input['fields'] ) && is_array( $input['fields'] ) ) { |
| 209 |
foreach ( $input['fields'] as $key => $value ) { |
| 210 |
if ( in_array( $key, $this->protected_keys, true ) ) { |
| 211 |
$ignored[] = $key; |
| 212 |
continue; |
| 213 |
} |
| 214 |
$data[ $key ] = $value; |
| 215 |
$applied[] = $key; |
| 216 |
} |
| 217 |
} |
| 218 |
|
| 219 |
// Apply the resolved identity + source/type and mark updated. |
| 220 |
$data['nx_id'] = $nx_id; |
| 221 |
$data['type'] = $type; |
| 222 |
$data['source'] = $source; |
| 223 |
$data['updated_at'] = current_time( 'mysql' ); |
| 224 |
unset( $data['update_status'] ); |
| 225 |
|
| 226 |
$post_type->save_post( $data ); |
| 227 |
|
| 228 |
return array( |
| 229 |
'nx_id' => $nx_id, |
| 230 |
'applied' => array_values( array_unique( $applied ) ), |
| 231 |
'ignored' => array_values( array_unique( $ignored ) ), |
| 232 |
'warnings' => $warnings, |
| 233 |
'notification' => $post_type->get_post( $nx_id ), |
| 234 |
); |
| 235 |
} |
| 236 |
} |
| 237 |
|