Notification::PT,
'request_key' => 'daytidy',
'nonce_name' => Notification::NONCE,
]);
}
public function init(): void
{
parent::init();
add_filter('manage_edit-' . $this->PT . '_sortable_columns', [$this, 'sortable']);
add_action('pre_get_posts', [$this, 'filterQuery']);
(new DetailsMetabox($this))->init();
(new PlaceholdersMetabox($this))->init();
}
public function args(): array
{
return apply_filters('DayTidy/F__DayTidy/Posts/Notification__args/args', [
'labels' => $this->labels(
esc_html__('Template', 'daytidy'),
esc_html__('Templates', 'daytidy')
),
'public' => false,
'show_ui' => true,
'show_in_menu' => false,
'show_in_rest' => false,
'show_in_admin_bar' => false,
'has_archive' => false,
'exclude_from_search' => true,
'publicly_queryable' => false,
'supports' => ['title'],
// Keep admin-only access primitive so WordPress does not register
// DayTidy settings access as a post meta capability.
'map_meta_cap' => false,
'capabilities' => Capabilities::postTypeCapabilities(Capabilities::MANAGE_SETTINGS),
'menu_icon' => 'dashicons-email-alt',
'menu_position' => 28,
]);
}
public function columns(array $columns): array
{
unset($columns['date']);
unset($columns['author']);
$columns['trigger'] = esc_html__('Trigger', 'daytidy');
$columns['type'] = esc_html__('Type', 'daytidy');
$columns['recipients'] = esc_html__('Recipients', 'daytidy');
$columns['enabled'] = esc_html__('Enabled', 'daytidy');
return $columns;
}
public function content(string $column_name, int $post_id): void
{
$notification = $this->entity($post_id);
if ($column_name === 'trigger')
{
$triggers = Registry::triggerOptions();
echo esc_html($triggers[$notification->trigger()] ?? $notification->trigger());
}
else if ($column_name === 'type')
{
$types = Registry::types();
echo esc_html($types[$notification->type()]['label'] ?? $notification->type());
}
else if ($column_name === 'enabled')
{
echo $notification->enabled()
? ''
: '';
}
else if ($column_name === 'recipients')
{
$labels = [];
$contexts = Registry::contexts($notification->trigger());
foreach ($contexts as $context => $details)
{
$template = $notification->template($context);
if (empty($template['active'])) continue;
$labels[] = $details['label'];
}
echo esc_html(count($labels) ? implode(', ', $labels) : __('None', 'daytidy'));
}
}
/**
* @param mixed[] $columns
* @return mixed[]
*/
public function sortable(array $columns): array
{
$columns['trigger'] = 'daytidy_notification_trigger';
$columns['type'] = 'daytidy_notification_type';
return $columns;
}
public function filterQuery(WP_Query $query): void
{
if (!is_admin() || !$query->is_main_query() || $query->get('post_type') !== $this->PT) return;
$trigger = isset($_REQUEST['daytidy_trigger']) ? sanitize_key(wp_unslash($_REQUEST['daytidy_trigger'])) : '';
if ($trigger !== '')
{
$meta_query = $query->get('meta_query');
$meta_query = is_array($meta_query) ? $meta_query : [];
$meta_query[] = [
'key' => 'daytidy_notification_trigger',
'value' => $trigger,
];
$query->set('meta_query', $meta_query);
}
$type = isset($_REQUEST['daytidy_type']) ? sanitize_key(wp_unslash($_REQUEST['daytidy_type'])) : '';
if ($type !== '')
{
$meta_query = $query->get('meta_query');
$meta_query = is_array($meta_query) ? $meta_query : [];
$meta_query[] = [
'key' => 'daytidy_notification_type',
'value' => $type,
];
$query->set('meta_query', $meta_query);
}
$orderby = $query->get('orderby');
if ($orderby === 'daytidy_notification_trigger' || $orderby === 'daytidy_notification_type')
{
$query->set('meta_key', $orderby);
$query->set('orderby', $orderby);
$order = $query->get('order');
$query->set('order', $order ?: 'asc');
}
}
public function filters(string $post_type): void
{
if ($post_type !== $this->PT) return;
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- All generated filter controls are passed through DayTidy Kses below.
$selected_trigger = isset($_GET['daytidy_trigger']) ? sanitize_text_field(wp_unslash($_GET['daytidy_trigger'])) : '';
$selected_type = isset($_GET['daytidy_type']) ? sanitize_text_field(wp_unslash($_GET['daytidy_type'])) : '';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- DayTidy Kses sanitizes the generated filter control markup.
echo Kses::form(Form::select([
'class' => 'wui-select daytidy-select',
'name' => 'daytidy_trigger',
'id' => 'daytidy-notification-filter-trigger',
'show_empty' => true,
'empty_label' => esc_html__('All Triggers', 'daytidy'),
'options' => $this->triggerFilterOptions(),
'value' => $selected_trigger,
]));
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- DayTidy Kses sanitizes the generated filter control markup.
echo Kses::form(Form::select([
'class' => 'wui-select daytidy-select',
'name' => 'daytidy_type',
'id' => 'daytidy-notification-filter-type',
'show_empty' => true,
'empty_label' => esc_html__('All Types', 'daytidy'),
'options' => $this->typeFilterOptions(),
'value' => $selected_type,
]));
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function store(Post $entity, array $data): void
{
$raw = $data;
$trigger = isset($raw['trigger']) ? sanitize_key($raw['trigger']) : Registry::TRIGGER_BOOKING_CREATED;
if (!array_key_exists($trigger, Registry::triggers())) $trigger = Registry::TRIGGER_BOOKING_CREATED;
$type = isset($raw['type']) ? sanitize_key($raw['type']) : Registry::TYPE_EMAIL;
if (!array_key_exists($type, Registry::types())) $type = Registry::TYPE_EMAIL;
$enabled = !empty($raw['enabled']) ? 1 : 0;
$templates = $this->sanitizeTemplates((array) ($raw['templates'] ?? []), $trigger);
$reminder_schedule = $this->sanitizeReminderSchedule((array) ($raw['reminder_schedule'] ?? []));
$entity->meta_store([
'daytidy_notification_trigger' => $trigger,
'daytidy_notification_type' => $type,
'daytidy_notification_enabled' => $enabled,
'daytidy_notification_templates' => $templates,
'daytidy_notification_reminder_schedule' => $reminder_schedule,
]);
do_action('DayTidy/A__DayTidy/Posts/Notification__store/data', $entity, $data, $this);
}
public function entity(int $id): NotificationEntity
{
return new NotificationEntity($id);
}
/**
* @param mixed[] $templates
* @return mixed[]
*/
private function sanitizeTemplates(array $templates, string $trigger): array
{
$sanitized = [];
foreach (Registry::contexts($trigger) as $context => $details)
{
$template = isset($templates[$context]) && is_array($templates[$context])
? $templates[$context]
: Defaults::blankTemplate($trigger, $context);
$sanitized[$context] = [
'active' => !empty($template['active']) ? 1 : 0,
'subject' => sanitize_text_field((string) ($template['subject'] ?? '')),
'content' => wp_kses_post((string) ($template['content'] ?? '')),
'to' => $this->sanitizePicker((array) ($template['to'] ?? [])),
'cc_enabled' => !array_key_exists('cc_enabled', $template) || !empty($template['cc_enabled']) ? 1 : 0,
'cc' => $this->sanitizePicker((array) ($template['cc'] ?? [])),
'bcc_enabled' => !array_key_exists('bcc_enabled', $template) || !empty($template['bcc_enabled']) ? 1 : 0,
'bcc' => $this->sanitizePicker((array) ($template['bcc'] ?? [])),
'attach_ical' => !empty($template['attach_ical']) ? 1 : 0,
'attachments' => $this->sanitizeAttachments((array) ($template['attachments'] ?? [])),
'collections' => $this->sanitizeCollections((array) ($template['collections'] ?? [])),
];
}
return $sanitized;
}
/**
* @param mixed[] $picker
* @return mixed[]
*/
private function sanitizePicker(array $picker): array
{
return [
'users' => isset($picker['users']) && is_array($picker['users'])
? array_values(array_filter(array_map('absint', $picker['users'])))
: [],
'roles' => isset($picker['roles']) && is_array($picker['roles'])
? array_values(array_filter(array_map('sanitize_key', $picker['roles'])))
: [],
'emails' => isset($picker['emails']) ? sanitize_textarea_field((string) $picker['emails']) : '',
];
}
/**
* @param mixed[] $attachments
* @return int[]
*/
private function sanitizeAttachments(array $attachments): array
{
return array_values(array_filter(array_map('absint', $attachments)));
}
/**
* @param mixed[] $collections
* @return string[]
*/
private function sanitizeCollections(array $collections): array
{
$sanitized = [];
foreach (Registry::collectionKeys() as $key)
{
$sanitized[$key] = wp_kses_post($this->decodeEntities((string) ($collections[$key] ?? '')));
}
return $sanitized;
}
/**
* @param mixed[] $schedule
* @return array{value: int, unit: string}
*/
private function sanitizeReminderSchedule(array $schedule): array
{
$unit = isset($schedule['unit']) ? sanitize_key((string) $schedule['unit']) : 'hours';
if (!in_array($unit, ['minutes', 'hours', 'days'], true)) $unit = 'hours';
return [
'value' => max(1, absint($schedule['value'] ?? 24)),
'unit' => $unit,
];
}
private function decodeEntities(string $value): string
{
for ($i = 0; $i < 5; $i++)
{
$decoded = html_entity_decode($value, ENT_QUOTES, get_bloginfo('charset') ?: 'UTF-8');
if ($decoded === $value) break;
$value = $decoded;
}
return $value;
}
/**
* @return string[]
*/
private function triggerFilterOptions(): array
{
$options = [];
foreach (Registry::triggerOptions() as $trigger => $label)
{
$options[$trigger] = sprintf(
'%s (%d)',
(string) $label,
$this->countByMeta('daytidy_notification_trigger', (string) $trigger)
);
}
return $options;
}
/**
* @return string[]
*/
private function typeFilterOptions(): array
{
$options = [];
foreach (Registry::types() as $type => $details)
{
$options[$type] = sprintf(
'%s (%d)',
(string) $details['label'],
$this->countByMeta('daytidy_notification_type', (string) $type)
);
}
return $options;
}
private function countByMeta(string $key, string $value): int
{
$posts = get_posts([
'post_type' => $this->PT,
'post_status' => ['publish', 'future', 'draft', 'pending', 'private'],
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => [
[
'key' => $key,
'value' => $value,
],
],
]);
return count($posts);
}
}