Acknowledgify. * It includes a tabbed interface for General, humans.txt, Meta Tags, and Footer Link settings. * * @package Acknowledgify */ // Define a namespace for the Admin_settings namespace Acknowledgify\Admin; use Acknowledgify\Includes\Helper; use Acknowledgify\Includes\Humans_txt; // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { exit; } // Load constants require_once plugin_dir_path( __FILE__ ) . '../includes/constants.php'; class Settings { /** * Renders the Acknowledgify Settings Page. */ public static function render_settings_page() { // Retrieve saved settings. $options = get_option( 'acknowledgify_settings' ); ?>
__( 'Choose a color', 'acknowledgify' ), 'invalid_color' => __( 'Invalid color selected. Please try again or refresh the page.', 'acknowledgify' ), ] ); // Enqueue the admin settings script. wp-a11y lets injected notices be // announced to screen readers. wp_enqueue_script( 'admin-settings-script', $plugin_url . 'js/admin-settings.js', [ 'jquery', 'wp-a11y' ], $plugin_version, // Explicit version number true ); // Translated labels shared by the humans.txt output and the live preview. $labels = Humans_txt::get_labels(); // Pass the PHP constants to JavaScript wp_localize_script( 'admin-settings-script', 'acknowledgify_vars', array( // Admin URL 'admin_url' => admin_url('admin-post.php'), 'nonce' => wp_create_nonce('acknowledgify_settings_nonce'), 'import_action' => ACKNOWLEDGIFY_IMPORT_ACTION, 'export_action' => ACKNOWLEDGIFY_EXPORT_ACTION, // Add humans.txt header text 'HEADER_FIXED' => ACKNOWLEDGIFY_HUMANS_TXT_HEADER, // Add freelancer specific labels 'FREELANCER_HEADER' => $labels['section_freelancer'] . "\n", 'FREELANCER_LABEL' => $labels['freelancer_name'], 'WEBSITE_LABEL' => $labels['freelancer_website'], 'EMAIL_LABEL' => $labels['freelancer_email'], 'LOCATION_LABEL' => $labels['freelancer_location'], 'PROJECT_CREDITS_HEADER' => "\n" . $labels['section_project_credits'] . "\n", 'PROJECT_LABEL' => $labels['freelancer_project'], 'ROLE_LABEL' => $labels['freelancer_role'], 'THANKS_TO_LABEL' => $labels['freelancer_thanks_to'], // Add agency specific labels 'AGENCY_HEADER' => $labels['section_team'] . "\n", 'AGENCY_LABEL' => $labels['agency_name'], 'AGENCY_WEBSITE_LABEL' => $labels['agency_website'], 'AGENCY_CONTACT_LABEL' => $labels['agency_contact'], 'AGENCY_LOCATION_LABEL' => $labels['agency_location'], 'AGENCY_PROJECT_LABEL' => $labels['agency_project'], 'AGENCY_DESIGN_LABEL' => $labels['agency_design'], 'AGENCY_DEVELOPMENT_LABEL' => $labels['agency_development'], 'AGENCY_SEO_LABEL' => $labels['agency_seo'], 'AGENCY_HOSTING_LABEL' => $labels['agency_hosting'], 'AGENCY_THANKS_LABEL' => $labels['agency_thanks'], // Alert Message 'MSG_SELECT_FILE' => __('Please select a JSON file to import.', 'acknowledgify'), 'MSG_IMPORT_SUCCESS' => __('Import successful!', 'acknowledgify'), 'MSG_IMPORT_FAILED' => __('Import failed. Check the console for details.', 'acknowledgify'), 'MSG_INVALID_JSON' => __('Invalid JSON file. Please check the file format.', 'acknowledgify'), 'MSG_DISMISS' => __('Dismiss this notice.', 'acknowledgify'), )); } /** * Sanitize a settings array against the known key allow-list. * * Shared by the settings form and the import handler so that both produce * identical option contents for identical input. Unrecognised keys are * dropped by construction: the returned array is built from a fixed set of * keys rather than copied from the input. * * @param array $input Raw settings, from a form post or a decoded import file. * @return array Sanitized settings. */ public static function sanitize_settings( $input ) { if ( ! is_array( $input ) ) { return array(); } $defaults = Helper::get_defaults(); // Sanitize settings $new_input = array(); // General Settings. $new_input['enable_humans_txt'] = empty( $input['enable_humans_txt'] ) ? 0 : 1; $new_input['enable_meta_tags'] = empty( $input['enable_meta_tags'] ) ? 0 : 1; $new_input['enable_footer_link'] = empty( $input['enable_footer_link'] ) ? 0 : 1; // humans.txt Settings. $new_input['humans_template'] = self::sanitize_choice( $input['humans_template'] ?? null, 'humans_template' ); $new_input['humans_custom_content'] = wp_kses_post( $input['humans_custom_content'] ?? '' ); // Freelancer Template Fields. $new_input['freelancer_name'] = sanitize_text_field( $input['freelancer_name'] ?? '' ); $new_input['freelancer_website'] = esc_url_raw( $input['freelancer_website'] ?? '' ); $new_input['freelancer_email'] = sanitize_email( $input['freelancer_email'] ?? '' ); $new_input['freelancer_location'] = sanitize_text_field( $input['freelancer_location'] ?? '' ); $new_input['freelancer_project'] = sanitize_text_field( $input['freelancer_project'] ?? '' ); $new_input['freelancer_role'] = sanitize_text_field( $input['freelancer_role'] ?? '' ); $new_input['freelancer_thanks_to'] = sanitize_text_field( $input['freelancer_thanks_to'] ?? '' ); // Agency Template Fields. $new_input['agency_name'] = sanitize_text_field( $input['agency_name'] ?? '' ); $new_input['agency_website'] = esc_url_raw( $input['agency_website'] ?? '' ); $new_input['agency_contact'] = sanitize_email( $input['agency_contact'] ?? '' ); $new_input['agency_location'] = sanitize_text_field( $input['agency_location'] ?? '' ); $new_input['agency_project'] = sanitize_text_field( $input['agency_project'] ?? '' ); $new_input['agency_design'] = sanitize_text_field( $input['agency_design'] ?? '' ); $new_input['agency_development'] = sanitize_text_field( $input['agency_development'] ?? '' ); $new_input['agency_seo'] = sanitize_text_field( $input['agency_seo'] ?? '' ); $new_input['agency_hosting'] = sanitize_text_field( $input['agency_hosting'] ?? '' ); $new_input['agency_thanks'] = sanitize_text_field( $input['agency_thanks'] ?? '' ); // Meta Tags Settings. $new_input['meta_author'] = sanitize_text_field( $input['meta_author'] ?? '' ); $new_input['meta_designer'] = sanitize_text_field( $input['meta_designer'] ?? '' ); $new_input['meta_developer'] = sanitize_text_field( $input['meta_developer'] ?? '' ); $new_input['meta_copyright'] = sanitize_text_field( $input['meta_copyright'] ?? '' ); $new_input['meta_owner'] = sanitize_text_field( $input['meta_owner'] ?? '' ); $new_input['meta_publisher'] = sanitize_text_field( $input['meta_publisher'] ?? '' ); $new_input['meta_manage'] = sanitize_text_field( $input['meta_manage'] ?? '' ); $new_input['meta_power'] = sanitize_text_field( $input['meta_power'] ?? '' ); $new_input['meta_acknowledgment'] = sanitize_text_field( $input['meta_acknowledgment'] ?? '' ); // Footer Link Settings. $new_input['footer_link_text'] = sanitize_text_field( $input['footer_link_text'] ?? $defaults['footer_link_text'] ); $new_input['footer_link_url'] = esc_url_raw( $input['footer_link_url'] ?? $defaults['footer_link_url'] ); $new_input['footer_custom_class'] = sanitize_text_field( $input['footer_custom_class'] ?? '' ); // sanitize_hex_color() returns null for anything malformed, which would // otherwise be stored and then printed into an inline style attribute. $color = sanitize_hex_color( $input['footer_text_color'] ?? '' ); $new_input['footer_text_color'] = ( null === $color || '' === $color ) ? $defaults['footer_text_color'] : $color; // Background is optional: an empty value means no background is emitted, // so it must stay empty rather than falling back to a default colour. $bg_color = sanitize_hex_color( (string) ( $input['footer_bg_color'] ?? '' ) ); $new_input['footer_bg_color'] = ( null === $bg_color ) ? '' : $bg_color; // Fixed-choice settings: anything outside the permitted set becomes the // default. footer_font_size is interpolated into an inline style // attribute by Footer_Link, so free text must never reach it. $new_input['footer_font_size'] = self::sanitize_choice( $input['footer_font_size'] ?? null, 'footer_font_size' ); $new_input['footer_placement_options'] = self::sanitize_choice( $input['footer_placement_options'] ?? null, 'footer_placement_options' ); $new_input['footer_style_mode'] = self::sanitize_choice( $input['footer_style_mode'] ?? null, 'footer_style_mode' ); $new_input['footer_alignment'] = self::sanitize_choice( $input['footer_alignment'] ?? null, 'footer_alignment' ); return $new_input; } /** * Sanitize 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; } public static function acknowledgify_export_settings() { if ( ! current_user_can('manage_options') ) { wp_die(esc_html__('You do not have permission to export settings.', 'acknowledgify'), 403); } if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) { wp_die(esc_html__('Invalid request method.', 'acknowledgify'), 405); } if ( ! isset($_GET['acknowledgify_nonce']) || ! wp_verify_nonce( sanitize_text_field( wp_unslash($_GET['acknowledgify_nonce']) ), 'acknowledgify_settings_nonce') ) { wp_die(esc_html__('Security check failed.', 'acknowledgify'), 403); } // Prevent output buffering issues if (ob_get_length()) { ob_end_clean(); } // Retrieve plugin settings $settings = get_option('acknowledgify_settings', array()); // 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_data = wp_json_encode( $settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); if ( false === $json_data ) { wp_die( esc_html__( 'Error exporting settings.', 'acknowledgify' ) ); } // Set headers for JSON file header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="acknowledgify-settings.json"' ); header( 'Content-Length: ' . strlen( $json_data ) ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); // Send JSON response. Already encoded above; escaping would corrupt it. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $json_data; exit; } /** * Transient key prefix for the notice shown after an import reloads the page. */ const IMPORT_NOTICE_TRANSIENT = 'acknowledgify_import_notice'; /** * Per-user transient key for the pending import notice. * * Scoped to the current user so a second administrator browsing any admin * screen cannot consume the notice meant for whoever ran the import. * * @return string */ private static function import_notice_key() { return self::IMPORT_NOTICE_TRANSIENT . '_' . get_current_user_id(); } /** * Handle a settings import. * * 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. */ public static function acknowledgify_import_settings() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', '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['acknowledgify_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['acknowledgify_nonce'] ) ), 'acknowledgify_settings_nonce' ) ) { wp_send_json_error( array( 'message' => __( 'Security check failed.', '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 sanitized per // field by sanitize_settings() below, which also applies the key // allow-list. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON payload; sanitized per field after decoding. $raw_settings = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : ''; if ( ! is_string( $raw_settings ) || '' === trim( $raw_settings ) ) { wp_send_json_error( array( 'message' => __( 'No settings provided.', 'acknowledgify' ) ), 400 ); } $settings = json_decode( $raw_settings, true ); if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $settings ) ) { wp_send_json_error( array( 'message' => __( 'Invalid JSON format. Please check your file.', 'acknowledgify' ) ), 400 ); } // Run through the same sanitiser the settings form uses, which applies // the key allow-list and the correct per-field treatment -- including // wp_kses_post() for the multi-line Custom template, which the previous // blanket sanitize_text_field() collapsed to a single line. $sanitized_array = self::sanitize_settings( $settings ); // A file with no keys we recognise is not an Acknowledgify export. $recognised = array_intersect( array_keys( $settings ), array_keys( $sanitized_array ) ); if ( empty( $recognised ) ) { wp_send_json_error( array( 'message' => __( 'No recognised Acknowledgify settings found in this file.', 'acknowledgify' ) ), 400 ); } update_option( 'acknowledgify_settings', $sanitized_array ); // The client reloads so the form shows the imported values; carry the // notice across that reload. set_transient( self::import_notice_key(), array( 'type' => 'success', 'message' => sprintf( /* translators: %d is the number of settings imported. */ _n( 'Settings imported successfully. %d setting was applied.', 'Settings imported successfully. %d settings were applied.', count( $recognised ), 'acknowledgify' ), count( $recognised ) ), ), MINUTE_IN_SECONDS ); wp_send_json_success( array( 'message' => __( 'Settings imported and saved successfully.', 'acknowledgify' ) ) ); } /** * Render the post-import admin notice, if one is pending. */ public static function render_import_notice() { $notice = get_transient( self::import_notice_key() ); if ( empty( $notice['message'] ) ) { return; } delete_transient( self::import_notice_key() ); $class = ( isset( $notice['type'] ) && 'success' === $notice['type'] ) ? 'notice notice-success is-dismissible' : 'notice notice-error is-dismissible'; printf( '%2$s