flareOps. Server-rendered, no build step. * * @package FlareOps */ namespace FlareOps; if ( ! defined( 'ABSPATH' ) ) { exit; } final class Admin { public const SLUG = 'flareops'; public const CAP = 'manage_woocommerce'; public static function cap(): string { return Util::woo_active() ? self::CAP : 'manage_options'; } public static function menu(): void { if ( Util::woo_active() ) { add_submenu_page( 'woocommerce', 'flareOps', 'flareOps', self::CAP, self::SLUG, array( __CLASS__, 'render' ) ); } else { add_options_page( 'flareOps', 'flareOps', 'manage_options', self::SLUG, array( __CLASS__, 'render' ) ); } } public static function page_url( array $args = array() ): string { $base = Util::woo_active() ? admin_url( 'admin.php?page=' . self::SLUG ) : admin_url( 'options-general.php?page=' . self::SLUG ); return add_query_arg( $args, $base ); } public const DISMISS_META = 'flareops_dismissed_notice'; /** * Notices are limited in scope (WordPress.org guideline 11): only on the Plugins screen, dismissible, and they * resolve themselves (WooCommerce active again / store connected). The flareOps page shows its own status. */ public static function notices(): void { if ( ! current_user_can( 'activate_plugins' ) ) { return; } $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'plugins' !== $screen->id ) { return; } $dismissed = (array) get_user_meta( get_current_user_id(), self::DISMISS_META, true ); $notice = ''; $key = ''; if ( ! Util::woo_active() ) { $key = 'woo'; $notice = esc_html__( 'WooCommerce is not active. Catalog, feed, fixes and product features are disabled until WooCommerce is activated.', 'flareops' ); } elseif ( ! Connect::is_connected() && ! Settings::is_poc_build() ) { $key = 'connect'; $notice = wp_kses_post( sprintf( /* translators: %s: link to the flareOps settings page */ __( 'Connect your store on the flareOps page to start. Nothing leaves this site until you connect.', 'flareops' ), esc_url( self::page_url() ) ) ); } if ( '' === $notice || ! empty( $dismissed[ $key ] ) ) { return; } $dismiss = wp_nonce_url( add_query_arg( array( 'action' => 'flareops_dismiss', 'notice' => $key ), admin_url( 'admin-post.php' ) ), 'flareops_dismiss' ); echo '

flareOps: ' . $notice . ' ' . esc_html__( 'Dismiss', 'flareops' ) . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $notice is escaped above. } public static function handle_dismiss(): void { if ( ! current_user_can( 'activate_plugins' ) ) { wp_die( esc_html__( 'Not allowed.', 'flareops' ), 403 ); } check_admin_referer( 'flareops_dismiss' ); $key = isset( $_GET['notice'] ) ? sanitize_key( wp_unslash( $_GET['notice'] ) ) : ''; if ( in_array( $key, array( 'woo', 'connect' ), true ) ) { $d = (array) get_user_meta( get_current_user_id(), self::DISMISS_META, true ); $d[ $key ] = 1; update_user_meta( get_current_user_id(), self::DISMISS_META, $d ); } wp_safe_redirect( admin_url( 'plugins.php' ) ); exit; } /* ---------- form handlers (admin-post.php) ---------- */ /** The fields of the settings form that handle_save() reads. */ private const SAVE_FIELDS = array( 'flareops_consent', 'features', 'treatment_ids', 'google_category_map', 'feed_shipping_country', 'feed_shipping_price', 'feed_shipping_service', 'shipping_text', 'returns_text', 'returns_days', 'feed_scope', 'merchant_google_verification', 'merchant_bing_verification', 'default_brand', 'content_any_product', 'flareops_google_verification', 'flareops_bing_verification', 'flareops_api_token', ); /** * Sanitize one posted settings field by its type. Values are validated again in Settings::update(). * * @param string $field Field name (one of SAVE_FIELDS). * @param mixed $raw Unslashed value. * @return mixed */ private static function sanitize_field( string $field, $raw ) { switch ( $field ) { case 'features': return is_array( $raw ) ? array_map( 'sanitize_key', $raw ) : array(); case 'treatment_ids': return array_values( array_filter( array_map( 'absint', (array) $raw ) ) ); case 'google_category_map': $out = array(); foreach ( is_array( $raw ) ? $raw : array() as $tid => $val ) { if ( is_scalar( $val ) ) { $out[ absint( $tid ) ] = sanitize_text_field( (string) $val ); } } return $out; case 'shipping_text': case 'returns_text': return is_scalar( $raw ) ? sanitize_textarea_field( (string) $raw ) : ''; case 'merchant_google_verification': case 'merchant_bing_verification': // The merchant may paste the whole tag: keep only that tag, then Settings parses it. return is_scalar( $raw ) ? trim( wp_kses( (string) $raw, array( 'meta' => array( 'name' => true, 'content' => true ) ) ) ) : ''; default: return is_scalar( $raw ) ? sanitize_text_field( (string) $raw ) : ''; } } public static function handle_save(): void { if ( ! current_user_can( self::cap() ) ) { wp_die( esc_html__( 'Not allowed.', 'flareops' ), 403 ); } check_admin_referer( 'flareops_save' ); // phpcs:disable WordPress.Security.NonceVerification // Only the form's own fields, never the whole request. Each value is validated below or in Settings::update(). $post = array(); foreach ( self::SAVE_FIELDS as $field ) { if ( isset( $_POST[ $field ] ) ) { $post[ $field ] = self::sanitize_field( $field, wp_unslash( $_POST[ $field ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized per field in sanitize_field(). } } $user = wp_get_current_user(); $want = ! empty( $post['flareops_consent'] ); if ( $want !== Settings::has_consent() ) { Settings::set_consent( $want, $user ? $user->user_login : '' ); } $features = array(); foreach ( Settings::FEATURES as $f ) { $features[ $f ] = ! empty( $post['features'][ $f ] ); } $ids = array_map( 'intval', (array) ( $post['treatment_ids'] ?? array() ) ); $ids = array_values( array_filter( $ids, static fn( $id ) => $id > 0 && 'product' === get_post_type( $id ) ) ); $cat_map = array(); foreach ( (array) ( $post['google_category_map'] ?? array() ) as $tid => $val ) { if ( is_string( $val ) && '' !== trim( $val ) ) { $cat_map[ (int) $tid ] = $val; } } $ship_country = strtoupper( trim( (string) ( $post['feed_shipping_country'] ?? '' ) ) ); $res = Settings::update( array( 'features' => $features, 'treatment_ids' => $ids, 'shipping_text' => (string) ( $post['shipping_text'] ?? '' ), 'returns_text' => (string) ( $post['returns_text'] ?? '' ), 'returns_days' => ( isset( $post['returns_days'] ) && '' !== trim( (string) $post['returns_days'] ) ) ? (int) $post['returns_days'] : null, 'feed_scope' => in_array( $post['feed_scope'] ?? '', Settings::FEED_SCOPES, true ) ? $post['feed_scope'] : 'treatment', 'merchant_google_verification' => (string) ( $post['merchant_google_verification'] ?? '' ), 'merchant_bing_verification' => (string) ( $post['merchant_bing_verification'] ?? '' ), 'default_brand' => (string) ( $post['default_brand'] ?? '' ), 'feed_shipping' => '' === $ship_country ? null : array( 'country' => $ship_country, 'price' => trim( (string) ( $post['feed_shipping_price'] ?? '' ) ), 'service' => (string) ( $post['feed_shipping_service'] ?? '' ), ), 'google_category_map' => $cat_map, 'content_any_product' => ! empty( $post['content_any_product'] ), ) ); if ( is_wp_error( $res ) ) { set_transient( 'flareops_admin_err_' . get_current_user_id(), $res->get_error_message(), 5 * MINUTE_IN_SECONDS ); } foreach ( ! Settings::is_poc_build() ? array() : array( 'FLAREOPS_GOOGLE_VERIFICATION' => 'flareops_google_verification', 'FLAREOPS_BING_VERIFICATION' => 'flareops_bing_verification', 'FLAREOPS_API_TOKEN' => 'flareops_api_token', ) as $const => $opt ) { if ( 'option' === Settings::config_source( $const ) && isset( $post[ $opt ] ) ) { $val = trim( sanitize_text_field( (string) $post[ $opt ] ) ); // The token field is never echoed back; an empty submit keeps the stored value. if ( 'flareops_api_token' === $opt && '' === $val ) { continue; } update_option( $opt, $val, true ); } } // phpcs:enable $msg = is_wp_error( $res ) ? 'error' : 'saved'; wp_safe_redirect( self::page_url( array( 'flareops_msg' => $msg ) ) ); exit; } public static function handle_undo(): void { if ( ! current_user_can( self::cap() ) ) { wp_die( esc_html__( 'Not allowed.', 'flareops' ), 403 ); } check_admin_referer( 'flareops_undo' ); $cid = isset( $_POST['change_id'] ) ? sanitize_text_field( wp_unslash( $_POST['change_id'] ) ) : ''; $res = Changes::undo( $cid, 'admin' ); wp_safe_redirect( self::page_url( array( 'flareops_msg' => is_wp_error( $res ) ? 'undo_failed' : 'undone' ) ) ); exit; } public static function handle_cache(): void { if ( ! current_user_can( self::cap() ) ) { wp_die( esc_html__( 'Not allowed.', 'flareops' ), 403 ); } check_admin_referer( 'flareops_cache' ); $undo = isset( $_POST['undo'] ); // phpcs:ignore WordPress.Security.NonceVerification -- checked above. if ( $undo ) { Cache::restore(); } else { Cache::apply(); } wp_safe_redirect( self::page_url( array( 'flareops_msg' => $undo ? 'cache_undone' : 'cache' ) ) ); exit; } /* ---------- render ---------- */ private static function yes( bool $b ): string { return $b ? '✓ yes' : '✗ no'; } public static function render(): void { if ( ! current_user_can( self::cap() ) ) { return; } $s = Settings::all(); $consent = Settings::consent_info(); $woo = Util::woo_active(); $labels = array( 'visible_stock_text' => __( 'Visible stock line under the price (treatment products)', 'flareops' ), 'shipping_returns_text' => __( 'Shipping & returns line (treatment products)', 'flareops' ), 'jsonld_enrich' => __( 'JSON-LD enrichment: brand, GTIN, MPN, return policy (treatment products)', 'flareops' ), 'feed_google' => __( 'Google Merchant Center feed', 'flareops' ), 'indexnow' => __( 'IndexNow pings on product save (needs consent)', 'flareops' ), 'bot_logging' => __( 'AI-bot visit logging', 'flareops' ), 'referral_beacon' => __( 'Cookieless AI-referral beacon', 'flareops' ), ); $msg = isset( $_GET['flareops_msg'] ) ? sanitize_key( wp_unslash( $_GET['flareops_msg'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification $messages = array( 'saved' => __( 'Settings saved.', 'flareops' ), 'error' => __( 'Some settings were invalid and were not saved.', 'flareops' ), 'undone' => __( 'Change undone.', 'flareops' ), 'undo_failed' => __( 'Undo failed.', 'flareops' ), 'cache' => __( 'AI bots now get fresh pages past your page cache.', 'flareops' ), 'cache_undone' => __( 'Your page cache settings were restored.', 'flareops' ), 'connected' => __( 'Connected to flareOps. Data sharing is on, as you approved.', 'flareops' ), 'connect_failed' => __( 'Connecting to flareOps failed.', 'flareops' ), 'disconnected' => __( 'Disconnected from flareOps. The site secret and verification tags were removed, and data sharing is off.', 'flareops' ), ); $err_key = 'flareops_admin_err_' . get_current_user_id(); $err = get_transient( $err_key ); if ( false !== $err ) { delete_transient( $err_key ); } $istate = IndexNow::state(); $cstate = get_option( Cache::STATE_OPTION, array() ); ?>

flareOps v

WooCommerce
Google   Bing
—


robots.txt
Google feed ()
IndexNow — :

Shipping and returns) and leave this empty. Fill it only to send one flat shipping rate in the feed.', 'flareops' ); ?>

'', 'price' => '', 'service' => '' ); ?>

' . esc_html__( 'Connection', 'flareops' ) . ''; if ( Connect::is_connected() ) { echo '

' . wp_kses_post( self::yes( true ) ) . ' ' . esc_html( sprintf( /* translators: 1: site id, 2: date, 3: user */ __( 'Connected to flareOps (store %1$s) since %2$s by %3$s.', 'flareops' ), Connect::site_id(), (string) ( $info['connected_at'] ?? '' ), (string) ( $info['by'] ?? '' ) ) ) . '

'; echo '

' . esc_html__( 'Data-sharing consent', 'flareops' ) . ': ' . wp_kses_post( self::yes( Settings::has_consent() ) ) . ' ' . esc_html__( '(given when you approved the connection on app.flareops.ai)', 'flareops' ) . '

'; echo '
'; wp_nonce_field( 'flareops_disconnect', 'flareops_disconnect_nonce' ); echo ''; echo ''; echo ' ' . esc_html__( 'Revokes the store key on flareOps, removes the verification tags and stops all data sharing.', 'flareops' ) . '
'; return; } echo '

' . esc_html__( 'Connect this store to your flareOps account. You will sign in on app.flareops.ai and approve the connection there. Until then this plugin sends nothing to any outside service.', 'flareops' ) . '

'; echo '
'; wp_nonce_field( 'flareops_connect', 'flareops_connect_nonce' ); echo ''; echo ''; echo ' ' . esc_html( sprintf( /* translators: %s: URL */ __( 'Opens %s', 'flareops' ), Connect::app_url() ) ) . '
'; } private static function render_config_fields(): void { if ( ! Settings::is_poc_build() ) { return; // Generic v1 build: the Connect flow sets everything. } $fields = array( 'FLAREOPS_GOOGLE_VERIFICATION' => array( 'flareops_google_verification', __( 'Google site-verification code', 'flareops' ) ), 'FLAREOPS_BING_VERIFICATION' => array( 'flareops_bing_verification', __( 'Bing msvalidate.01 code', 'flareops' ) ), 'FLAREOPS_API_TOKEN' => array( 'flareops_api_token', __( 'API bearer token', 'flareops' ) ), ); $editable = array_filter( array_keys( $fields ), static fn( $c ) => 'option' === Settings::config_source( $c ) ); if ( ! $editable ) { return; } echo '

' . esc_html__( 'Connection (not set by the build)', 'flareops' ) . '

'; foreach ( $editable as $c ) { [ $opt, $label ] = $fields[ $c ]; if ( 'flareops_api_token' === $opt ) { // Never echo the token into the page; show only whether one is set. $is_set = '' !== (string) get_option( $opt, '' ); echo '

'; continue; } echo '

'; } } private static function render_category_map( array $map ): void { if ( ! Util::woo_active() ) { return; } $terms = get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'number' => 300, ) ); if ( ! is_array( $terms ) || ! $terms ) { echo '

' . esc_html__( 'No product categories.', 'flareops' ) . '

'; return; } echo '

' . esc_html__( 'Optional. Enter a Google product category ID (e.g. 736) or full path for a category; subcategories inherit it. Unmapped categories send no google_product_category (Google then picks one itself).', 'flareops' ) . '

'; echo ''; foreach ( $terms as $t ) { $tid = (int) $t->term_id; $val = (string) ( $map[ $tid ] ?? '' ); printf( '', esc_html( Products::category_path_for_term( $tid ) ), (int) $tid, esc_attr( $val ) ); } echo '
%1$s #%2$d
'; } private static function render_picker( array $selected ): void { if ( ! Util::woo_active() ) { echo '

' . esc_html__( 'WooCommerce is inactive.', 'flareops' ) . '

'; return; } $products = wc_get_products( array( 'status' => array( 'publish', 'private', 'draft' ), 'limit' => 500, 'orderby' => 'title', 'order' => 'ASC', ) ); if ( ! $products ) { echo '

' . esc_html__( 'No products yet.', 'flareops' ) . '

'; return; } echo '

' . esc_html( sprintf( /* translators: %d: count */ __( '%d selected. Unchecked products are the control group.', 'flareops' ), count( $selected ) ) ) . '

'; echo '
'; foreach ( $products as $p ) { printf( '', (int) $p->get_id(), checked( in_array( $p->get_id(), $selected, true ), true, false ), esc_html( $p->get_name() ), esc_html( $p->get_sku() ? 'SKU ' . $p->get_sku() : '' ) ); } echo '
'; } private static function render_changes(): void { $items = Util::safe( static fn() => Changes::list_since( null, 50 ), array() ); if ( ! $items ) { echo '

' . esc_html__( 'No changes yet.', 'flareops' ) . '

'; return; } $groups = array(); foreach ( $items as $it ) { $groups[ $it['change_id'] ][] = $it; } echo ''; foreach ( $groups as $cid => $rows ) { foreach ( $rows as $i => $r ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } } echo '
WhenProductFieldBeforeAfterSourceApproved by
' . esc_html( (string) $r['at'] ) . '#' . (int) $r['product_id'] . '' . esc_html( $r['field'] ) . '' . esc_html( mb_strimwidth( $r['before'], 0, 80, '…' ) ) . '' . esc_html( mb_strimwidth( $r['after'], 0, 80, '…' ) ) . '' . esc_html( $r['source'] ) . '' . esc_html( $r['approved_by'] ) . ''; if ( 0 === $i ) { if ( $r['undone_at'] ) { echo '' . esc_html__( 'undone', 'flareops' ) . ''; } else { echo '
'; wp_nonce_field( 'flareops_undo' ); echo ''; echo '
'; } } echo '
'; } }