default value. */ public static function get_defaults() { return array( // Schema bookkeeping. '_schema_version' => ACKNOWLEDGIFY_SCHEMA_VERSION, // General. 'enable_humans_txt' => 0, 'enable_meta_tags' => 0, 'enable_footer_link' => 0, // humans.txt. 'humans_template' => 'freelancer', 'humans_custom_content' => '', // Freelancer template. 'freelancer_name' => '', 'freelancer_website' => '', 'freelancer_email' => '', 'freelancer_location' => '', 'freelancer_project' => '', 'freelancer_role' => '', 'freelancer_thanks_to' => '', // Agency template. The four fixed role fields that used to sit // here (design, development, seo, hosting) are now credit rows. 'agency_name' => '', 'agency_website' => '', 'agency_contact' => '', 'agency_location' => '', 'agency_project' => '', 'agency_thanks' => '', // Independent lists. Each row has role, name and url keys. 'agency_credits' => array(), 'freelancer_credits' => array(), // Meta tags. 'meta_author' => '', 'meta_designer' => '', 'meta_developer' => '', 'meta_copyright' => '', 'meta_owner' => '', 'meta_publisher' => '', 'meta_manage' => '', 'meta_power' => '', 'meta_acknowledgment' => '', // Footer link. 'footer_link_text' => 'Made by Acknowledgify', 'footer_link_url' => 'https://acknowledgify.com', 'footer_text_color' => '#000000', 'footer_bg_color' => '', // Empty means no background is emitted. 'footer_alignment' => 'inherit', // 'inherit' emits no text-align. 'footer_font_size' => '16px', 'footer_custom_class' => '', 'footer_placement_options' => 'auto_insert', 'footer_style_mode' => 'plugin', ); } /** * Permitted values for the fixed-choice settings. * * Anything outside these sets falls back to the default rather than being * stored. footer_font_size matters most: it is interpolated into an inline * style attribute by Footer_Link, so it must never accept free text. * * @return array Setting key => list of permitted values. */ public static function get_allowed_values() { return array( 'humans_template' => array( 'freelancer', 'agency', 'custom' ), 'footer_placement_options' => array( 'auto_insert', 'shortcode', 'php_snippet' ), 'footer_font_size' => array( '8px', '9px', '10px', '11px', '12px', '13px', '14px', '15px', '16px', '17px', '18px', '20px' ), 'footer_style_mode' => array( 'plugin', 'external' ), 'footer_alignment' => array( 'inherit', 'left', 'center', 'right' ), ); } /** * Default for a single setting. * * @param string $key The setting key. * @return mixed The default, or '' when the key is unknown. */ public static function get_default( $key ) { $defaults = self::get_defaults(); return array_key_exists( $key, $defaults ) ? $defaults[ $key ] : ''; } /** * All stored settings, memoised for the request. * * @return array */ public static function get_settings() { if ( null === self::$cache ) { $stored = get_option( ACKNOWLEDGIFY_OPTION, array() ); self::$cache = is_array( $stored ) ? $stored : array(); } return self::$cache; } /** * Drop the memoised copy. * * Call this after every write to the option, or the rest of the request * keeps reading pre-write values. * * @return void */ public static function flush_cache() { self::$cache = null; } /** * Get a plugin setting. * * Uses array_key_exists() rather than ! empty(). The old check meant a * deliberately cleared field fell back to its default, so "no footer text" * silently became "Made by Acknowledgify" and an empty value could never be * saved. An empty string that the user chose is a real value and must be * returned as one; the migration back-fills the two footer fields where * that change would otherwise alter what an existing site renders. * * @param string $key The setting key. * @param mixed $fallback Optional. Value to use when the key is absent. * Omit to use the registered default for the key. * @return mixed */ public static function get_setting( $key, $fallback = null ) { if ( null === $fallback ) { $fallback = self::get_default( $key ); } $options = self::get_settings(); return array_key_exists( $key, $options ) ? $options[ $key ] : $fallback; } /** * Stored credit rows, normalised. * * Guarantees an array of rows each with role, name and url string keys, so * callers never have to defend against a half-formed row. * * @param string|null $template Agency or freelancer; defaults to the selected template. * @return array[] List of array( 'role' => string, 'name' => string, 'url' => string ). */ public static function get_credits( $template = null ) { $template = null === $template ? self::get_setting( 'humans_template' ) : $template; if ( ! in_array( $template, array( 'agency', 'freelancer' ), true ) ) { return array(); } $raw = self::get_setting( $template . '_credits', array() ); if ( ! is_array( $raw ) ) { return array(); } $credits = array(); foreach ( $raw as $row ) { if ( ! is_array( $row ) ) { continue; } $credits[] = array( 'role' => isset( $row['role'] ) ? (string) $row['role'] : '', 'name' => isset( $row['name'] ) ? (string) $row['name'] : '', 'url' => isset( $row['url'] ) ? (string) $row['url'] : '', ); } return $credits; } /** * Normalise a value for inclusion in a plain-text response. * * The file is served as text/plain, so HTML escaping is wrong there: * there is no HTML context for entities to be decoded in, and "Smith & * Jones" would reach the reader as "Smith & Jones". The appropriate * protections for plain text are the Content-Type header, normalised line * endings and the removal of control characters. Values are already * sanitised on input. * * @param string $value Raw setting value. * @param bool $allow_newlines Whether line breaks may be preserved. * @return string */ public static function plain_text( $value, $allow_newlines = false ) { $value = (string) $value; // Normalise CRLF and lone CR to LF so output is consistent regardless // of where the value was authored. $value = str_replace( array( "\r\n", "\r" ), "\n", $value ); if ( $allow_newlines ) { // Strip control characters except newline and tab. return (string) preg_replace( '/[^\P{C}\n\t]/u', '', $value ); } // Single-line field: fold any line breaks into spaces, then strip all // remaining control characters. $value = str_replace( "\n", ' ', $value ); return trim( (string) preg_replace( '/[^\P{C}\t]/u', '', $value ) ); } /** * Format one setting as a "Label: value" humans.txt line. * * Placeholder tokens are resolved before the plain-text normalisation, so * a token that resolves to text containing an ampersand or a line break is * handled by the same rules as text the user typed. * * @param string $key The setting key. * @param string $label The label to prepend. * @return string The formatted line including a trailing newline, or ''. */ public static function format_setting( $key, $label ) { return self::format_line( $label, self::get_setting( $key, '' ) ); } /** * Format an arbitrary label and value as a humans.txt line. * * @param string $label Label text. * @param string $value Raw value, may contain tokens. * @return string The formatted line including a trailing newline, or ''. */ public static function format_line( $label, $value ) { $value = self::plain_text( Tokens::resolve( $value ) ); $label = self::plain_text( $label ); if ( '' === $value || '' === $label ) { return ''; } return $label . ': ' . $value . "\n"; } /** * Build one acknowledgment meta tag. * * @param string $key The setting key. * @param string $meta_name The meta name attribute. * @return string The tag including a trailing newline, or '' when unset. */ public static function generate_meta_tag( $key, $meta_name ) { $value = trim( Tokens::resolve( (string) self::get_setting( $key, '' ) ) ); if ( '' === $value ) { return ''; } return sprintf( '' . "\n", esc_attr( $meta_name ), esc_attr( $value ) ); } /** * Bring a stored settings array up to the current schema version. * * Pure and idempotent: it takes an array and returns an array, touching no * globals, so the same routine upgrades the live option, an imported file * and a unit test. * * @param array $settings Stored or imported settings. * @return array Settings at ACKNOWLEDGIFY_SCHEMA_VERSION. */ public static function migrate( $settings ) { if ( ! is_array( $settings ) ) { return array(); } // A site with nothing saved has no history to preserve; it picks up // the defaults on first save. if ( empty( $settings ) ) { return $settings; } $version = isset( $settings['_schema_version'] ) ? (int) $settings['_schema_version'] : 1; if ( $version >= ACKNOWLEDGIFY_SCHEMA_VERSION ) { return $settings; } $shared = isset( $settings['credits'] ) && is_array( $settings['credits'] ) ? $settings['credits'] : array(); if ( $version < 2 ) { $defaults = self::get_defaults(); // Preserve the old footer appearance only when importing/upgrading // schema 1. Current forms submit schema 3 and never enter this path. if ( ! array_key_exists( 'footer_style_mode', $settings ) ) { $placement = strtolower( str_replace( '-', '_', (string) ( $settings['footer_placement_options'] ?? 'auto_insert' ) ) ); $settings['footer_style_mode'] = 'auto_insert' === $placement ? 'plugin' : 'external'; } foreach ( array( 'footer_link_text', 'footer_link_url' ) as $key ) { if ( array_key_exists( $key, $settings ) && '' === trim( (string) $settings[ $key ] ) ) { $settings[ $key ] = $defaults[ $key ]; } } $legacy_roles = array( 'agency_design' => __( 'Design', 'acknowledgify' ), 'agency_development' => __( 'Development', 'acknowledgify' ), 'agency_seo' => __( 'SEO & Performance', 'acknowledgify' ), 'agency_hosting' => __( 'Hosting & Maintenance', 'acknowledgify' ), ); $legacy = array(); foreach ( $legacy_roles as $key => $label ) { $value = isset( $settings[ $key ] ) ? trim( (string) $settings[ $key ] ) : ''; if ( '' !== $value ) { $legacy[] = array( 'role' => $label, 'name' => $value, 'url' => '', ); } unset( $settings[ $key ] ); } // Agency-only history must never become visible in Freelancer. $settings['agency_credits'] = array_merge( $legacy, $shared ); $settings['freelancer_credits'] = array(); } else { // Schema 2 intentionally shared rows. Their original template cannot // be inferred; seed both lists to preserve existing output, then keep // subsequent edits independent. Never guess which rows to delete. foreach ( array( 'agency_credits', 'freelancer_credits' ) as $key ) { if ( ! array_key_exists( $key, $settings ) ) { $settings[ $key ] = $shared; } } } unset( $settings['credits'] ); $settings['_schema_version'] = ACKNOWLEDGIFY_SCHEMA_VERSION; return $settings; } }