Acknowledgify.
*
* Every form control is rendered through one of the render_*_row() helpers
* below. That is deliberate: the previous version built each row by hand and
* several inputs ended up with no associated label, because the visible text
* lived in a
that was never tied to the field. Going through a helper
* makes a label mandatory by construction.
*
* @package Acknowledgify
*/
namespace Acknowledgify\Admin;
use Acknowledgify\Includes\Helper;
use Acknowledgify\Includes\Humans_Txt;
use Acknowledgify\Includes\Meta_Tags;
use Acknowledgify\Includes\Tokens;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Renders the settings screen and handles saving, export and import.
*/
class Settings {
/**
* Nonce action and field name shared by export and import.
*/
const NONCE_ACTION = 'acknowledgify_settings_nonce';
const NONCE_NAME = 'acknowledgify_nonce';
/**
* Transient prefix for a notice that has to survive a page reload.
*/
const NOTICE_TRANSIENT = 'acknowledgify_notice';
/**
* Tab slugs, in display order.
*
* @return array Slug => array( 'label' => string, 'toggle' => string|null ).
*/
private static function get_tabs() {
return array(
'general' => array(
'label' => __( 'General', 'acknowledgify' ),
'toggle' => null,
),
'humans' => array(
'label' => __( 'humans.txt', 'acknowledgify' ),
'toggle' => 'enable_humans_txt',
),
'meta' => array(
'label' => __( 'Meta Tags', 'acknowledgify' ),
'toggle' => 'enable_meta_tags',
),
'footer' => array(
'label' => __( 'Footer Link', 'acknowledgify' ),
'toggle' => 'enable_footer_link',
),
);
}
/**
* Human-readable labels for the meta tag fields.
*
* @return array Setting key => label.
*/
private static function get_meta_labels() {
return array(
'meta_author' => __( 'Author', 'acknowledgify' ),
'meta_designer' => __( 'Designer', 'acknowledgify' ),
'meta_developer' => __( 'Developer', 'acknowledgify' ),
'meta_copyright' => __( 'Copyright', 'acknowledgify' ),
'meta_owner' => __( 'Owner', 'acknowledgify' ),
'meta_publisher' => __( 'Publisher', 'acknowledgify' ),
'meta_manage' => __( 'Managed by', 'acknowledgify' ),
'meta_power' => __( 'Powered by', 'acknowledgify' ),
'meta_acknowledgment' => __( 'Acknowledgment', 'acknowledgify' ),
);
}
/**
* DOM id for a setting's control.
*
* @param string $key Setting key.
* @return string
*/
private static function field_id( $key ) {
return 'ack-' . str_replace( '_', '-', $key );
}
/**
* Field name attribute for a setting.
*
* @param string $key Setting key.
* @return string
*/
private static function field_name( $key ) {
return ACKNOWLEDGIFY_OPTION . '[' . $key . ']';
}
/**
* Which tab to open.
*
* Read from the query string so the tab survives the round trip through
* options.php, which redirects without the fragment. This only selects
* which panel is visible, so it needs sanitising against the tab list but
* no nonce.
*
* @return string Tab slug.
*/
private static function get_active_tab() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display preference only, validated against a fixed list.
$requested = isset( $_GET['ack_tab'] ) ? sanitize_key( wp_unslash( $_GET['ack_tab'] ) ) : '';
return array_key_exists( $requested, self::get_tabs() ) ? $requested : 'general';
}
/**
* Print the description and code sample that follow a control.
*
* @param array $args Row arguments.
* @return void
*/
private static function render_help( $args ) {
if ( '' !== $args['description'] ) {
printf(
'%2$s
',
esc_attr( $args['describedby'] ),
esc_html( $args['description'] )
);
}
if ( '' !== $args['code'] ) {
printf( '%s
', esc_html( $args['code'] ) );
}
}
/**
* Shared argument handling for every row helper.
*
* @param string $key Setting key.
* @param string $label Visible label text.
* @param array $args Optional overrides.
* @return array
*/
private static function prepare_row( $key, $label, $args ) {
$args = wp_parse_args(
$args,
array(
'type' => 'text',
'description' => '',
'code' => '',
'placeholder' => '',
'class' => 'regular-text',
'row_id' => '',
'tokens' => false,
)
);
if ( $args['tokens'] && '' !== $args['description'] ) {
$args['description'] .= ' ' . __( 'Placeholder tokens work here.', 'acknowledgify' );
}
$args['id'] = self::field_id( $key );
$args['name'] = self::field_name( $key );
$args['label'] = $label;
$args['describedby'] = $args['id'] . '-description';
return $args;
}
/**
* Open a form-table row and print its label cell.
*
* @param array $args Prepared row arguments.
* @return void
*/
private static function open_row( $args ) {
printf(
' %s ',
$args['row_id'] ? ' id="' . esc_attr( $args['row_id'] ) . '"' : '',
esc_attr( $args['id'] ),
esc_html( $args['label'] )
);
}
/**
* Render a single-line input row.
*
* @param string $key Setting key.
* @param string $label Visible label.
* @param array $args Optional overrides.
* @return void
*/
private static function render_text_row( $key, $label, $args = array() ) {
$args = self::prepare_row( $key, $label, $args );
self::open_row( $args );
printf(
' ',
esc_attr( $args['type'] ),
esc_attr( $args['id'] ),
esc_attr( $args['name'] ),
esc_attr( (string) Helper::get_setting( $key, '' ) ),
esc_attr( $args['class'] ),
$args['placeholder'] ? ' placeholder="' . esc_attr( $args['placeholder'] ) . '"' : '',
'' !== $args['description'] ? ' aria-describedby="' . esc_attr( $args['describedby'] ) . '"' : ''
);
self::render_help( $args );
echo ' ';
}
/**
* Render a textarea row.
*
* @param string $key Setting key.
* @param string $label Visible label.
* @param array $args Optional overrides.
* @return void
*/
private static function render_textarea_row( $key, $label, $args = array() ) {
$args = self::prepare_row(
$key,
$label,
wp_parse_args(
$args,
array(
'rows' => 8,
'class' => 'large-text code',
)
)
);
self::open_row( $args );
printf(
'',
esc_attr( $args['id'] ),
esc_attr( $args['name'] ),
(int) $args['rows'],
esc_attr( $args['class'] ),
'' !== $args['description'] ? ' aria-describedby="' . esc_attr( $args['describedby'] ) . '"' : '',
esc_textarea( (string) Helper::get_setting( $key, '' ) )
);
self::render_help( $args );
echo '';
}
/**
* Render a select row.
*
* Options come from Helper::get_allowed_values(), so the control can only
* ever offer values the sanitiser will accept.
*
* @param string $key Setting key.
* @param string $label Visible label.
* @param array $labels Value => visible option text.
* @param array $args Optional overrides.
* @return void
*/
private static function render_select_row( $key, $label, $labels, $args = array() ) {
$args = self::prepare_row( $key, $label, $args );
self::open_row( $args );
printf(
'',
esc_attr( $args['id'] ),
esc_attr( $args['name'] ),
'' !== $args['description'] ? ' aria-describedby="' . esc_attr( $args['describedby'] ) . '"' : ''
);
$current = Helper::get_setting( $key );
$allowed = Helper::get_allowed_values();
$values = isset( $allowed[ $key ] ) ? $allowed[ $key ] : array_keys( $labels );
foreach ( $values as $value ) {
printf(
'%3$s ',
esc_attr( $value ),
selected( $current, $value, false ),
esc_html( $labels[ $value ] ?? $value )
);
}
echo ' ';
self::render_help( $args );
echo '';
}
/**
* Render a colour picker row.
*
* @param string $key Setting key.
* @param string $label Visible label.
* @param array $args Optional overrides.
* @return void
*/
private static function render_color_row( $key, $label, $args = array() ) {
$args = self::prepare_row(
$key,
$label,
wp_parse_args( $args, array( 'class' => 'regular-text ack-color-picker' ) )
);
self::open_row( $args );
printf(
' ',
esc_attr( $args['id'] ),
esc_attr( $args['name'] ),
esc_attr( (string) Helper::get_setting( $key, '' ) ),
esc_attr( $args['class'] ),
esc_attr( (string) Helper::get_default( $key ) ),
'' !== $args['description'] ? ' aria-describedby="' . esc_attr( $args['describedby'] ) . '"' : ''
);
self::render_help( $args );
echo '';
}
/**
* Render a feature toggle.
*
* The switch appearance is driven entirely by :checked in CSS. The previous
* version mirrored the state into a data-toggle attribute with JavaScript,
* which meant the visual state was wrong without JavaScript, and gave the
* checkbox an empty so a screen reader announced no name at all.
*
* @param string $key Setting key.
* @param string $label Switch label text.
* @param string $description Help text.
* @return void
*/
private static function render_toggle_row( $key, $label, $description ) {
$id = self::field_id( $key );
$describedby = $id . '-description';
?>
/>
%3$s
',
esc_attr( $toggle_key ),
(int) Helper::get_setting( $toggle_key, 0 ) === 1 ? ' hidden' : '',
esc_html__( 'This feature is switched off, so nothing is published yet. You can still fill everything in here, then turn it on under General.', 'acknowledgify' )
);
}
/**
* Render the collapsible placeholder token reference.
*
* Discoverability matters more than documentation here: the table lists
* every token alongside what it resolves to on this site, so there is
* nothing to look up and no guessing about what a token will produce.
*
* @return void
*/
private static function render_token_reference() {
$reference = Tokens::get_reference();
if ( empty( $reference ) ) {
return;
}
?>
$details ) : ?>
%%%%
' . esc_html__( 'not set', 'acknowledgify' ) . ''; ?>
array(
'label' => __( 'Role', 'acknowledgify' ),
'placeholder' => __( 'Photography', 'acknowledgify' ),
'list' => 'ack-role-suggestions-' . $template,
'type' => 'text',
),
'name' => array(
'label' => __( 'Credit to', 'acknowledgify' ),
'placeholder' => __( 'Jo Bloggs', 'acknowledgify' ),
'list' => '',
'type' => 'text',
),
'url' => array(
'label' => __( 'Website (optional)', 'acknowledgify' ),
'placeholder' => 'https://',
'list' => '',
'type' => 'text',
),
);
?>
'',
'name' => '',
'url' => '',
);
?>
$credit ) {
self::render_credit_row( (int) $index, $credit, $template );
}
?>
>
Humans_Txt::get_header(),
'sections' => array(
'freelancer' => array(
'identityLabel' => $labels['section_freelancer'],
'creditsLabel' => $labels['section_project_credits'],
'identity' => array(
array(
'id' => self::field_id( 'freelancer_name' ),
'label' => $labels['freelancer_name'],
),
array(
'id' => self::field_id( 'freelancer_website' ),
'label' => $labels['freelancer_website'],
),
array(
'id' => self::field_id( 'freelancer_email' ),
'label' => $labels['freelancer_email'],
),
array(
'id' => self::field_id( 'freelancer_location' ),
'label' => $labels['freelancer_location'],
),
),
'before' => array(
array(
'id' => self::field_id( 'freelancer_project' ),
'label' => $labels['freelancer_project'],
),
array(
'id' => self::field_id( 'freelancer_role' ),
'label' => $labels['freelancer_role'],
),
),
'after' => array(
array(
'id' => self::field_id( 'freelancer_thanks_to' ),
'label' => $labels['freelancer_thanks_to'],
),
),
),
'agency' => array(
'identityLabel' => $labels['section_team'],
'creditsLabel' => $labels['section_project_credits'],
'identity' => array(
array(
'id' => self::field_id( 'agency_name' ),
'label' => $labels['agency_name'],
),
array(
'id' => self::field_id( 'agency_website' ),
'label' => $labels['agency_website'],
),
array(
'id' => self::field_id( 'agency_contact' ),
'label' => $labels['agency_contact'],
),
array(
'id' => self::field_id( 'agency_location' ),
'label' => $labels['agency_location'],
),
),
'before' => array(
array(
'id' => self::field_id( 'agency_project' ),
'label' => $labels['agency_project'],
),
),
'after' => array(
array(
'id' => self::field_id( 'agency_thanks' ),
'label' => $labels['agency_thanks'],
),
),
),
'custom' => array(
'sourceId' => self::field_id( 'humans_custom_content' ),
),
),
);
wp_localize_script(
'acknowledgify-admin',
'acknowledgifyAdmin',
array(
'adminPost' => admin_url( 'admin-post.php' ),
'pageUrl' => admin_url( 'options-general.php?page=acknowledgify' ),
'nonce' => wp_create_nonce( self::NONCE_ACTION ),
'exportAction' => ACKNOWLEDGIFY_EXPORT_ACTION,
'importAction' => ACKNOWLEDGIFY_IMPORT_ACTION,
'maxCredits' => Helper::max_credits(),
'preview' => $preview,
'tokens' => Tokens::get_values(),
'i18n' => array(
'selectFile' => __( 'Choose a JSON file to import first.', 'acknowledgify' ),
'importFailed' => __( 'Import failed. Nothing has been changed.', 'acknowledgify' ),
'readFailed' => __( 'That file could not be read. Try exporting it again.', 'acknowledgify' ),
'dismiss' => __( 'Dismiss this notice.', 'acknowledgify' ),
'creditAdded' => __( 'Credit row added.', 'acknowledgify' ),
'creditRemoved' => __( 'Credit row removed.', 'acknowledgify' ),
'creditsCopied' => __( 'Credits copied. Save changes to keep them.', 'acknowledgify' ),
'copyTooMany' => __( 'There is not enough room to copy all credits. Remove some rows and try again.', 'acknowledgify' ),
'maxReached' => __( 'You have reached the maximum number of credits.', 'acknowledgify' ),
/* translators: %s is a row number. */
'creditNumber' => __( 'Credit %s', 'acknowledgify' ),
/* translators: %s is a row number. */
'removeCredit' => __( 'credit %s', 'acknowledgify' ),
),
)
);
}
/**
* Sanitise a settings array against the known key allow-list.
*
* Shared by the settings form and the import handler, so both produce
* identical option contents for identical input. Unrecognised keys are
* dropped by construction: the result is built from a fixed set of keys
* rather than copied from the input.
*
* @param mixed $input Raw settings from a form post or a decoded import file.
* @return array Sanitised settings.
*/
public static function sanitize_settings( $input ) {
if ( ! is_array( $input ) ) {
return array();
}
// An older export may still carry the pre-1.2.0 shape, so bring it up
// to date before reading anything out of it.
$input = Helper::migrate( $input );
$defaults = Helper::get_defaults();
$clean = array( '_schema_version' => ACKNOWLEDGIFY_SCHEMA_VERSION );
// General: an unchecked checkbox is absent from the post, not false.
foreach ( array( 'enable_humans_txt', 'enable_meta_tags', 'enable_footer_link' ) as $key ) {
$clean[ $key ] = empty( $input[ $key ] ) ? 0 : 1;
}
// humans.txt.
$clean['humans_template'] = self::sanitize_choice( $input['humans_template'] ?? null, 'humans_template' );
$clean['humans_custom_content'] = wp_kses_post( (string) ( $input['humans_custom_content'] ?? '' ) );
// Plain single-line text fields.
$text_keys = array(
'freelancer_name',
'freelancer_location',
'freelancer_project',
'freelancer_role',
'freelancer_thanks_to',
'agency_name',
'agency_location',
'agency_project',
'agency_thanks',
'meta_author',
'meta_designer',
'meta_developer',
'meta_copyright',
'meta_owner',
'meta_publisher',
'meta_manage',
'meta_power',
'meta_acknowledgment',
'footer_custom_class',
);
foreach ( $text_keys as $key ) {
$clean[ $key ] = sanitize_text_field( (string) ( $input[ $key ] ?? '' ) );
}
// URLs and email addresses.
$clean['freelancer_website'] = Tokens::sanitize_url( $input['freelancer_website'] ?? '' );
$clean['agency_website'] = Tokens::sanitize_url( $input['agency_website'] ?? '' );
$clean['freelancer_email'] = sanitize_email( (string) ( $input['freelancer_email'] ?? '' ) );
$clean['agency_contact'] = sanitize_email( (string) ( $input['agency_contact'] ?? '' ) );
// Credits.
foreach ( array( 'agency_credits', 'freelancer_credits' ) as $key ) {
$clean[ $key ] = self::sanitize_credits( $input[ $key ] ?? array() );
}
// Footer text. footer_link_text keeps its limited HTML subset, so it
// goes through the same allow-list the front end uses rather than
// sanitize_text_field(), which would strip the tags on save.
$clean['footer_link_text'] = wp_kses(
(string) ( $input['footer_link_text'] ?? $defaults['footer_link_text'] ),
array(
'strong' => array(),
'em' => array(),
'b' => array(),
'i' => array(),
'u' => array(),
'span' => array( 'class' => true ),
'div' => array( 'class' => true ),
)
);
$clean['footer_link_url'] = Tokens::sanitize_url( $input['footer_link_url'] ?? $defaults['footer_link_url'] );
// sanitize_hex_color() returns null for anything malformed, which
// would otherwise be stored and then printed into an inline style.
$text_color = sanitize_hex_color( (string) ( $input['footer_text_color'] ?? '' ) );
$clean['footer_text_color'] = ( null === $text_color || '' === $text_color )
? $defaults['footer_text_color']
: $text_color;
// Background is optional: empty means no background is emitted, so it
// must stay empty rather than falling back to a colour.
$bg_color = sanitize_hex_color( (string) ( $input['footer_bg_color'] ?? '' ) );
$clean['footer_bg_color'] = ( null === $bg_color ) ? '' : $bg_color;
// Fixed-choice settings. footer_font_size matters most: it is
// interpolated into an inline style attribute, so free text must never
// reach it.
foreach ( array( 'footer_font_size', 'footer_placement_options', 'footer_style_mode', 'footer_alignment' ) as $key ) {
$clean[ $key ] = self::sanitize_choice( $input[ $key ] ?? null, $key );
}
return $clean;
}
/**
* Sanitise a fixed-choice setting against its allow-list.
*
* @param mixed $value Raw incoming value.
* @param string $key Setting key, used to look up permitted values.
* @return string A permitted value, or the setting's default.
*/
private static function sanitize_choice( $value, $key ) {
$allowed = Helper::get_allowed_values();
$default = Helper::get_default( $key );
if ( ! isset( $allowed[ $key ] ) ) {
return sanitize_text_field( (string) $value );
}
$value = is_scalar( $value ) ? sanitize_text_field( (string) $value ) : '';
return in_array( $value, $allowed[ $key ], true ) ? $value : $default;
}
/**
* Sanitise the credits repeater.
*
* Rebuilt and reindexed from scratch, so posted array keys can never
* become stored keys. A row with nothing in it at all is dropped; a
* part-filled row is kept, because losing a half-typed row on save is
* worse than storing one that happens to output nothing.
*
* @param mixed $raw Posted or imported credits.
* @return array[]
*/
private static function sanitize_credits( $raw ) {
if ( ! is_array( $raw ) ) {
return array();
}
$clean = array();
$max = Helper::max_credits();
foreach ( $raw as $row ) {
if ( count( $clean ) >= $max ) {
break;
}
if ( ! is_array( $row ) ) {
continue;
}
$role = sanitize_text_field( (string) ( $row['role'] ?? '' ) );
$name = sanitize_text_field( (string) ( $row['name'] ?? '' ) );
$url = Tokens::sanitize_url( $row['url'] ?? '' );
if ( '' === $role && '' === $name && '' === $url ) {
continue;
}
$clean[] = array(
'role' => $role,
'name' => $name,
'url' => $url,
);
}
return $clean;
}
/**
* Per-user transient key for a pending notice.
*
* Scoped to the current user so a second administrator browsing any admin
* screen cannot consume a notice meant for whoever ran the import.
*
* @return string
*/
private static function notice_key() {
return self::NOTICE_TRANSIENT . '_' . get_current_user_id();
}
/**
* Download every setting as a JSON file.
*
* @return void
*/
public static function handle_export() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to export these settings.', 'acknowledgify' ), 403 );
}
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
wp_die( esc_html__( 'Invalid request method.', 'acknowledgify' ), 405 );
}
if ( ! isset( $_GET[ self::NONCE_NAME ] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ self::NONCE_NAME ] ) ), self::NONCE_ACTION ) ) {
wp_die( esc_html__( 'Security check failed. Reload the settings page and try again.', 'acknowledgify' ), 403 );
}
$settings = get_option( ACKNOWLEDGIFY_OPTION, array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
// Stamp the schema version so a file exported today can still be
// imported after a future schema change.
$settings['_schema_version'] = ACKNOWLEDGIFY_SCHEMA_VERSION;
// Encode once. Measuring one encoder's output while echoing another's
// produces a wrong Content-Length whenever the two differ in byte
// length, which truncates the download.
$json = wp_json_encode( $settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
if ( false === $json ) {
wp_die( esc_html__( 'These settings could not be encoded for export.', 'acknowledgify' ) );
}
if ( ob_get_length() ) {
ob_end_clean();
}
$filename = 'acknowledgify-settings-' . gmdate( 'Y-m-d' ) . '.json';
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . strlen( $json ) );
nocache_headers();
// Already JSON-encoded; escaping would corrupt it.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $json;
exit;
}
/**
* Replace every setting from an uploaded JSON file.
*
* Every failure path returns the same JSON shape, a data.message string
* alongside an accurate HTTP status, so the client can report the real
* reason instead of guessing.
*
* @return void
*/
public static function handle_import() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'You do not have permission to import settings.', 'acknowledgify' ) ), 403 );
}
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
wp_send_json_error( array( 'message' => __( 'Invalid request method.', 'acknowledgify' ) ), 405 );
}
if ( ! isset( $_POST[ self::NONCE_NAME ] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ self::NONCE_NAME ] ) ), self::NONCE_ACTION ) ) {
wp_send_json_error( array( 'message' => __( 'Security check failed. Reload the settings page and try again.', 'acknowledgify' ) ), 403 );
}
/*
* Read the payload once. It is a JSON document, so it cannot be passed
* through a string sanitiser before decoding without corrupting the
* serialisation format itself. The decoded values are sanitised per
* field by sanitize_settings() below, which also applies the key
* allow-list.
*/
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON payload; sanitised per field after decoding.
$raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : '';
if ( ! is_string( $raw ) || '' === trim( $raw ) ) {
wp_send_json_error( array( 'message' => __( 'No settings were sent.', 'acknowledgify' ) ), 400 );
}
// A settings export is a few kilobytes. Refuse anything absurd rather
// than handing it to json_decode().
if ( strlen( $raw ) > 256 * KB_IN_BYTES ) {
wp_send_json_error( array( 'message' => __( 'That file is too large to be an Acknowledgify export.', 'acknowledgify' ) ), 413 );
}
$decoded = json_decode( $raw, true );
if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $decoded ) ) {
wp_send_json_error( array( 'message' => __( 'That file is not valid JSON. Export it again from the other site.', 'acknowledgify' ) ), 400 );
}
if ( isset( $decoded['_schema_version'] ) && (int) $decoded['_schema_version'] > ACKNOWLEDGIFY_SCHEMA_VERSION ) {
wp_send_json_error( array( 'message' => __( 'This export needs a newer version of Acknowledgify.', 'acknowledgify' ) ), 400 );
}
$sanitized = self::sanitize_settings( $decoded );
// A file with none of our keys in it is not an Acknowledgify export.
$recognised = array_intersect( array_keys( $decoded ), array_merge( array_keys( Helper::get_defaults() ), array( 'credits', 'agency_design', 'agency_development', 'agency_seo', 'agency_hosting' ) ) );
if ( empty( $recognised ) ) {
wp_send_json_error( array( 'message' => __( 'No Acknowledgify settings were found in that file.', 'acknowledgify' ) ), 400 );
}
update_option( ACKNOWLEDGIFY_OPTION, $sanitized );
Helper::flush_cache();
// update_option() also returns false for unchanged settings, so compare
// the actual stored value instead of treating that return as failure.
if ( get_option( ACKNOWLEDGIFY_OPTION, array() ) !== $sanitized ) {
wp_send_json_error( array( 'message' => __( 'Settings could not be saved. Please try again.', 'acknowledgify' ) ), 500 );
}
$count = count( $recognised );
set_transient(
self::notice_key(),
array(
'type' => 'success',
'message' => sprintf(
/* translators: %d is the number of settings imported. */
_n(
'Settings imported. %d setting was applied.',
'Settings imported. %d settings were applied.',
$count,
'acknowledgify'
),
$count
),
),
MINUTE_IN_SECONDS
);
wp_send_json_success( array( 'message' => __( 'Settings imported.', 'acknowledgify' ) ) );
}
/**
* Render a pending notice after an import reloads the page.
*
* @return void
*/
public static function render_admin_notices() {
$notice = get_transient( self::notice_key() );
if ( empty( $notice['message'] ) ) {
return;
}
delete_transient( self::notice_key() );
$class = ( isset( $notice['type'] ) && 'success' === $notice['type'] )
? 'notice notice-success is-dismissible'
: 'notice notice-error is-dismissible';
printf(
'',
esc_attr( $class ),
esc_html( $notice['message'] )
);
}
}