Add message details, per-row actions, selection-aware bulk controls and confirmation dialogs. Merge filters into the list card and link overview statistics to the matching views with site-timezone today filtering. Improve the compose dialog with Unicode character counters, recipient mode segments, role counts and explicit broadcast confirmation verified against the resolved role and recipient count on submission. Split rendering into focused sibling traits and cover rendering, handlers and UI interactions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
164 lines
9.7 KiB
PHP
164 lines
9.7 KiB
PHP
<?php
|
|
/** WordPress-free message admin render / handler / SQL harness. No actual notifications are sent. */
|
|
namespace AKSRT\Core {
|
|
final class DBMigrator {
|
|
public static function table($key) { return 'wp_aksrt_' . $key; }
|
|
}
|
|
}
|
|
namespace AKSRT\Messages {
|
|
final class Message {
|
|
public static $sent = [];
|
|
public static function get_instance() { return new self(); }
|
|
public function send($id, $type, $content, $args) {
|
|
self::$sent[] = compact('id', 'type', 'content', 'args');
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
namespace {
|
|
define('ABSPATH', __DIR__);
|
|
define('ARRAY_A', 'ARRAY_A');
|
|
$root = dirname(__DIR__, 2);
|
|
define('AKSRT_DIR', $root);
|
|
define('AKSRT_URI', 'https://example.test/theme');
|
|
class WP_Error {
|
|
public function __construct(public $code, public $message) {}
|
|
public function get_error_message() { return $this->message; }
|
|
}
|
|
class WP_User {
|
|
public $ID = 7;
|
|
public $display_name = 'Test <user>';
|
|
}
|
|
class Redirect extends \RuntimeException {}
|
|
function __($text, $domain = '') { return $text; }
|
|
function esc_html($text) { return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8'); }
|
|
function esc_attr($text) { return esc_html($text); }
|
|
function esc_html__($text, $domain) { return esc_html($text); }
|
|
function esc_attr__($text, $domain) { return esc_attr($text); }
|
|
function esc_url($url) { return preg_match('/^javascript:/i', $url) ? '' : esc_attr($url); }
|
|
function esc_url_raw($url) { return preg_match('/^javascript:/i', $url) ? '' : $url; }
|
|
function sanitize_key($text) { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($text)); }
|
|
function sanitize_text_field($text) { return trim(strip_tags($text)); }
|
|
function sanitize_textarea_field($text) { return trim(strip_tags($text)); }
|
|
function sanitize_user($text) { return $text; }
|
|
function wp_unslash($text) { return $text; }
|
|
function absint($value) { return abs((int) $value); }
|
|
function is_wp_error($value) { return $value instanceof WP_Error; }
|
|
function is_email($value) { return filter_var($value, FILTER_VALIDATE_EMAIL); }
|
|
function admin_url($path) { return 'https://example.test/wp-admin/' . $path; }
|
|
function get_edit_user_link($id) { return admin_url('user-edit.php?user_id=' . $id); }
|
|
function get_avatar_url($id, $args) { return 'https://example.test/avatar.png'; }
|
|
function get_userdata($id) { return $id > 0 ? new WP_User() : false; }
|
|
function get_user_by($key, $value) { return new WP_User(); }
|
|
function cache_users($ids) { $GLOBALS['cache_calls'][] = $ids; }
|
|
function number_format_i18n($count) { return number_format($count); }
|
|
function human_time_diff($time) { return '1 分钟'; }
|
|
function checked($actual, $expected, $echo = true) { return $actual === $expected ? ' checked' : ''; }
|
|
function selected($actual, $expected, $echo = true) { return $actual === $expected ? ' selected' : ''; }
|
|
function wp_nonce_field($action) { echo '<input type="hidden" name="_wpnonce" value="' . esc_attr($action) . '" />'; }
|
|
function check_admin_referer($action) { $GLOBALS['nonce_checked'][] = $action; }
|
|
function wp_roles() {
|
|
return new class {
|
|
public $roles = ['subscriber' => [], 'editor' => [], 'author' => [], 'contributor' => []];
|
|
public function get_names() { return ['subscriber' => '订阅者', 'editor' => '编辑', 'author' => '作者', 'contributor' => '贡献者']; }
|
|
};
|
|
}
|
|
function translate_user_role($role) { return $role; }
|
|
function count_users($strategy) { return ['avail_roles' => ['subscriber' => 3, 'editor' => 501, 'author' => 0, 'contributor' => 500]]; }
|
|
function get_users($args) { return array_slice(range(1, $GLOBALS['role_count'] ?? 3), 0, $args['number']); }
|
|
function wp_safe_redirect($url) { throw new Redirect($url); }
|
|
function add_query_arg($args, $url) { return $url . '?' . http_build_query($args); }
|
|
function submit_button($text, $type, $name, $wrap) { echo '<button type="submit">' . esc_html($text) . '</button>'; }
|
|
function current_datetime() { return new \DateTimeImmutable($GLOBALS['now'] ?? '2026-10-30 12:00:00', new \DateTimeZone($GLOBALS['zone'] ?? 'Asia/Shanghai')); }
|
|
function get_gmt_from_date($local) { return (new \DateTimeImmutable($local, new \DateTimeZone($GLOBALS['zone'] ?? 'Asia/Shanghai')))->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); }
|
|
function get_date_from_gmt($gmt, $format) { return (new \DateTimeImmutable($gmt, new \DateTimeZone('UTC')))->setTimezone(new \DateTimeZone('Asia/Shanghai'))->format($format); }
|
|
function current_time($format, $gmt = false) { return '2026-10-30 04:00:00'; }
|
|
class Database {
|
|
public $prepared = [];
|
|
public $queries = [];
|
|
public function prepare($sql, ...$params) {
|
|
$params = count($params) === 1 && is_array($params[0]) ? $params[0] : $params;
|
|
$this->prepared[] = ['sql' => $sql, 'params' => $params];
|
|
return $sql;
|
|
}
|
|
public function get_var($sql) { return 2; }
|
|
public function get_results($sql, $format) { return []; }
|
|
public function get_row($sql, $format) { return ['total' => 2, 'unread' => 1, 'today' => 1, 'recipients' => 2]; }
|
|
public function get_col($sql) { return ['system']; }
|
|
public function query($sql) { $this->queries[] = $sql; return 1; }
|
|
public function esc_like($text) { return addcslashes($text, '_%\\'); }
|
|
}
|
|
$wpdb = new Database();
|
|
require $root . '/src/Messages/MessageTypes.php';
|
|
require $root . '/src/Messages/AdminBroadcast.php';
|
|
require $root . '/src/Messages/NotificationAdminQuery.php';
|
|
require $root . '/src/Admin/Native/NativeUi.php';
|
|
require $root . '/src/Admin/Native/UsersMenusMessagesList.php';
|
|
require $root . '/src/Admin/Native/UsersMenusMessagesCompose.php';
|
|
require $root . '/src/Admin/Native/UsersMenusMessages.php';
|
|
class Probe {
|
|
use \AKSRT\Admin\Native\UsersMenusMessages;
|
|
public $cap_checked = 0;
|
|
private function ensure_cap(): void { $this->cap_checked++; }
|
|
public function invoke($method, ...$args) { return $this->$method(...$args); }
|
|
public function html($method, ...$args) {
|
|
ob_start(); $this->$method(...$args); return ob_get_clean();
|
|
}
|
|
}
|
|
$probe = new Probe();
|
|
$out = [];
|
|
$filters = $probe->invoke('message_filters', ['period' => 'today', 'status' => 'unread', 's' => 'a%_']);
|
|
$out['filters'] = $filters;
|
|
$out['bad_period'] = $probe->invoke('message_filters', ['period' => 'evil']);
|
|
$out['query_filters'] = $probe->invoke('message_query_filters', $filters);
|
|
$out['compose'] = $probe->html('render_message_compose');
|
|
$out['dialogs'] = $probe->html('render_message_dialogs');
|
|
$out['overview'] = $probe->html('render_message_overview');
|
|
$out['filter_form'] = $probe->html('render_message_filter_form', $filters);
|
|
$out['views'] = $probe->invoke('message_views_html', $filters, ['all' => 2, 'unread' => 1, 'read' => 1]);
|
|
$row = ['id' => 8, 'user_id' => 7, 'type' => 'system', 'title' => '<script>alert(1)</script>', 'description' => str_repeat('完整内容', 100) . "\n<script>x</script>", 'target_url' => 'javascript:alert(1)', 'is_read' => 0, 'created_at' => '2026-10-30 03:00:00', 'read_at' => null];
|
|
$out['table'] = $probe->html('render_message_table', [$row, array_merge($row, ['id' => 9, 'is_read' => 1, 'read_at' => '2026-10-30 03:30:00'])], $filters, 3, false);
|
|
$out['empty'] = $probe->html('render_message_table', [], $filters, 1, false);
|
|
$out['cache_calls'] = $GLOBALS['cache_calls'];
|
|
|
|
$wpdb->prepared = [];
|
|
\AKSRT\Messages\NotificationAdminQuery::paginate($out['query_filters'], 1);
|
|
\AKSRT\Messages\NotificationAdminQuery::status_counts($out['query_filters']);
|
|
\AKSRT\Messages\NotificationAdminQuery::overview();
|
|
$out['sql'] = $wpdb->prepared;
|
|
$GLOBALS['zone'] = 'America/New_York';
|
|
$GLOBALS['now'] = '2026-11-01 12:00:00';
|
|
$wpdb->prepared = [];
|
|
\AKSRT\Messages\NotificationAdminQuery::overview();
|
|
$out['dst_sql'] = $wpdb->prepared;
|
|
|
|
$base = ['recipient_mode' => 'role', 'role' => 'subscriber', 'content' => 'Hello', 'title' => 'Title'];
|
|
$cases = [
|
|
'unconfirmed' => $base,
|
|
'changed_count' => $base + ['confirmed_role' => 'subscriber', 'confirmed_count' => 2],
|
|
'changed_role' => $base + ['confirmed_role' => 'editor', 'confirmed_count' => 3],
|
|
'confirmed' => $base + ['confirmed_role' => 'subscriber', 'confirmed_count' => 3],
|
|
'too_large' => $base + ['confirmed_role' => 'subscriber', 'confirmed_count' => 501],
|
|
'users' => ['recipient_mode' => 'users', 'users' => '7', 'content' => 'Hello'],
|
|
'long_title' => array_merge($base, ['title' => str_repeat('字', 192)]),
|
|
];
|
|
foreach ($cases as $name => $post) {
|
|
$_POST = $post;
|
|
$GLOBALS['role_count'] = $name === 'too_large' ? 501 : 3;
|
|
\AKSRT\Messages\Message::$sent = [];
|
|
try { $probe->handle_send_message(); } catch (Redirect $redirect) {
|
|
parse_str(parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
|
|
$out['send'][$name] = ['redirect' => $query, 'sent' => count(\AKSRT\Messages\Message::$sent)];
|
|
}
|
|
}
|
|
$_POST = ['bulk_action' => 'delete', 'message_id' => 8, 'ids' => [9, 10], 'period' => 'today', 'status' => 'unread', 'paged' => 3];
|
|
$wpdb->prepared = [];
|
|
try { $probe->handle_bulk_messages(); } catch (Redirect $redirect) {
|
|
parse_str(parse_url($redirect->getMessage(), PHP_URL_QUERY), $query);
|
|
$out['single_delete'] = ['redirect' => $query, 'sql' => $wpdb->prepared];
|
|
}
|
|
$out['cap_checked'] = $probe->cap_checked;
|
|
$out['nonce_checked'] = $GLOBALS['nonce_checked'];
|
|
echo json_encode($out, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
|
}
|