[
'delField' => __('Delete this field? Existing content is not deleted, but the front end will no longer be able to read it.', 'aumframe'),
'delType' => __('Delete this type? Existing content is NOT deleted, but it will disappear from the admin (nothing registers the type any more). Are you sure?', 'aumframe'),
],
], JSON_UNESCAPED_UNICODE) . ';', 'before');
}
// ------------------------------------------------------------------ Saving
public static function handle_post(): void
{
if (empty($_POST['aumframe_ct_action']) || !current_user_can('manage_options')) {
return;
}
check_admin_referer('aumframe_ct_model', self::NONCE);
$action = sanitize_key(wp_unslash($_POST['aumframe_ct_action']));
$notice = '';
if ($action === 'save') {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Store::save() hands the whole structure to Store::sanitize_model(), which sanitises each post type and taxonomy.
$raw = isset($_POST['model']) && is_array($_POST['model']) ? wp_unslash($_POST['model']) : [];
$model = [
'postTypes' => array_values((array) ($raw['postTypes'] ?? [])),
'taxonomies' => array_values((array) ($raw['taxonomies'] ?? [])),
];
Store::save($model);
$notice = 'saved';
}
if ($action === 'import') {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, unslashed then decoded by Store::import(), which runs the same sanitize_model().
$json = isset($_POST['import_json']) ? wp_unslash($_POST['import_json']) : '';
$merge = empty($_POST['import_replace']);
$result = Store::import((string) $json, $merge);
$notice = $result['ok'] ? 'imported-' . $result['added'] . '-' . $result['skipped'] : 'import-failed';
}
wp_safe_redirect(add_query_arg(
['page' => self::PAGE, 'notice' => $notice],
admin_url('admin.php')
));
exit;
}
// ------------------------------------------------------------------ Rendering
public static function render(): void
{
if (!current_user_can('manage_options')) {
return;
}
// Export is an ordinary GET — no AJAX download button, since copy and paste is used more often anyway
if (isset($_GET['export'])) {
self::render_export();
return;
}
$model = Store::all();
?>
', esc_html__('Saved. Permalinks have been flushed.', 'aumframe'));
return;
}
if ($notice === 'import-failed') {
printf('
%s
', esc_html__('Import failed: that is not a valid model pack.', 'aumframe'));
return;
}
if (strpos($notice, 'imported-') === 0) {
$parts = explode('-', $notice);
printf(
'
%s
',
esc_html(sprintf(
/* translators: 1: Number of content types added. 2: Number skipped because a definition already existed. */
__('Import done: %1$d added, %2$d skipped (existing definitions kept).', 'aumframe'),
(int) ($parts[1] ?? 0),
(int) ($parts[2] ?? 0)
))
);
}
}
// ------------------------------------------------------------------ Cards
/**
* The card for an extension entry — a content-model entry that only attaches fields to a type
* somebody else registered (the built-in `page` / `post`, or another plugin's type).
*
* It needs its own card for two reasons, and the second one loses data:
*
* - Nothing registration-time applies. Labels, URL prefix, menu icon, visibility, archive and
* supported areas all belong to whoever registered the type. The ordinary card offers all of them,
* and `sanitize_post_type()` drops every one of them again on save — "set it and nothing happened".
* - `extends` has to come back on submit. The form is the whole entry: whatever it does not
* render is not in `$_POST`, and `sanitize_post_type()` reads `$pt['extends']` to decide which
* shape it is looking at. An extension entry rendered through the ordinary card came back as an
* ordinary one, with no labels — and an ordinary entry with no labels is discarded. So opening
* this page and pressing Save deleted every extension entry, reporting success.
*/
private static function extension_card(array $pt, string $i): void
{
$n = 'model[postTypes][' . $i . ']';
$name = (string) ($pt['name'] ?? '');
?>