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 '
'; // 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 — :
' . 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 '';
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 '';
}
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 '' . esc_html( $label ) . '
';
continue;
}
echo '' . esc_html( $label ) . '
';
}
}
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 '';
}
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(
' %3$s #%1$d %4$s ',
(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 'When Product Field Before After Source Approved by ';
foreach ( $groups as $cid => $rows ) {
foreach ( $rows as $i => $r ) {
echo '';
echo '' . esc_html( (string) $r['at'] ) . ' ';
echo '#' . (int) $r['product_id'] . ' ';
echo '' . esc_html( $r['field'] ) . ' ';
echo '' . esc_html( mb_strimwidth( $r['before'], 0, 80, '…' ) ) . ' ';
echo '' . esc_html( mb_strimwidth( $r['after'], 0, 80, '…' ) ) . ' ';
echo '' . esc_html( $r['source'] ) . ' ';
echo '' . esc_html( $r['approved_by'] ) . ' ';
echo '';
if ( 0 === $i ) {
if ( $r['undone_at'] ) {
echo '' . esc_html__( 'undone', 'flareops' ) . ' ';
} else {
echo '';
}
}
echo ' ';
}
}
echo '
';
}
}