'', 'brand_logo_id' => 0, 'brand_color' => '#0f6b50', 'footer_text' => '', 'hide_credit' => true, 'credit_opt_in' => false, 'email_from_name' => '', 'email_reply_to' => '', 'alerts_enabled' => true, 'alert_emails' => array(), 'alert_user_ids' => array(), 'email_defaults' => array( 'frequency' => 'monthly', 'weekday' => 1, 'monthday' => 3, 'hour' => 8, 'compare' => 'previous', ), 'warm_cache' => true, 'monitor_enabled' => true, 'monitor_sensitivity' => 'standard', 'brief_enabled' => false, 'brief_weekday' => 1, 'brief_hour' => 8, 'brief_emails' => array(), 'ai_enabled' => false, 'ai_provider' => 'openai', 'ai_model' => 'gpt-5-mini', 'ai_daily_limit' => 30, 'ai_monthly_limit' => 300, 'ai_max_output' => 1200, // Who confirmed that client data may go to which provider, and when. 'ai_consent' => array(), ); } /** * All settings merged over defaults. * * @return array */ public function all() { $blog = get_current_blog_id(); if ( $blog !== $this->memo_blog ) { $this->memo = null; $this->memo_blog = $blog; } if ( null === $this->memo ) { $stored = get_option( self::OPTION, array() ); $this->memo = array_merge( self::defaults(), is_array( $stored ) ? $stored : array() ); } return $this->memo; } /** * One setting. * * @param string $key Setting key. * @return mixed */ public function get( $key ) { $all = $this->all(); return array_key_exists( $key, $all ) ? $all[ $key ] : null; } /** Explicit permission to display credits in reports. @return bool */ public function show_credit() { return true === $this->get( 'credit_opt_in' ) && ! $this->get( 'hide_credit' ); } /** * Saves settings. Unknown keys are dropped. * * @param array $values New values. * @return void */ public function save( array $values ) { OptionUpdate::change( self::OPTION, static function ( array $current ) use ( $values ) { return array_intersect_key( array_merge( self::defaults(), $current, $values ), self::defaults() ); } ); $this->memo = null; } /** * Brand name, falling back to the WordPress site title. * * @return string */ public function brand_name() { $name = (string) $this->get( 'brand_name' ); return '' !== $name ? $name : (string) get_bloginfo( 'name' ); } /** * Brand colour as a validated hex value. * * @return string */ public function brand_color() { $color = (string) $this->get( 'brand_color' ); return preg_match( '/^#[0-9a-fA-F]{6}$/', $color ) ? $color : '#0f6b50'; } /** * Brand logo URL, or an empty string. * * @return string */ public function logo_url() { return self::attachment_url( (int) $this->get( 'brand_logo_id' ) ); } /** * Schedule new sites start with (interval, day, time, comparison). Off until * someone switches it on and picks recipients. * * @return array Normalised schedule. */ public function email_defaults() { $defaults = (array) $this->get( 'email_defaults' ); return \TurtleReport\Policy\Schedule::normalize( array( 'frequency' => isset( $defaults['frequency'] ) ? $defaults['frequency'] : 'monthly', 'weekday' => isset( $defaults['weekday'] ) ? $defaults['weekday'] : 1, 'monthday' => isset( $defaults['monthday'] ) ? $defaults['monthday'] : 3, 'hour' => isset( $defaults['hour'] ) ? $defaults['hour'] : 8, 'compare' => isset( $defaults['compare'] ) ? $defaults['compare'] : 'previous', ) ); } /** * Addresses that receive connection alerts: the chosen users plus any typed * addresses. Falls back to the site admin email when nobody is chosen. * * @return string[] */ public function alert_recipients() { $list = array(); foreach ( (array) $this->get( 'alert_user_ids' ) as $user_id ) { $user = get_userdata( (int) $user_id ); if ( $user && is_email( $user->user_email ) ) { $list[] = (string) $user->user_email; } } $list = array_merge( $list, array_filter( (array) $this->get( 'alert_emails' ), 'is_email' ) ); $list = array_values( array_unique( array_map( 'strtolower', $list ) ) ); return $list ? $list : array( (string) get_option( 'admin_email' ) ); } /** * Local file of an image attachment (the medium size when there is one), for * server-made PDFs. Empty when it isn't an image or isn't stored locally * (for example on an offloaded media library). * * @param int $id Attachment ID. * @return string */ public static function attachment_file( $id ) { $id = (int) $id; if ( $id <= 0 || ! function_exists( 'get_attached_file' ) || ! wp_attachment_is_image( $id ) ) { return ''; } $full = (string) get_attached_file( $id ); if ( function_exists( 'image_get_intermediate_size' ) ) { $medium = image_get_intermediate_size( $id, 'medium' ); if ( is_array( $medium ) && ! empty( $medium['path'] ) ) { $uploads = wp_upload_dir( null, false ); $path = trailingslashit( $uploads['basedir'] ) . $medium['path']; if ( is_readable( $path ) ) { return $path; } } } return '' !== $full && is_readable( $full ) ? $full : ''; } /** * URL of an image attachment, or an empty string. * * @param int $id Attachment ID. * @return string */ public static function attachment_url( $id ) { if ( $id <= 0 || ! wp_attachment_is_image( $id ) ) { return ''; } $src = wp_get_attachment_image_src( $id, 'medium' ); return $src ? (string) $src[0] : ''; } }