settings = $settings; add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_action( 'admin_init', array( $this, 'save_settings' ) ); add_action( 'admin_post_rafgrid_export_xlsx', array( $this, 'download_xlsx' ) ); add_action( 'admin_post_rafgrid_labels', array( $this, 'labels_page' ) ); add_action( 'admin_post_rafgrid_download_export', array( $this, 'download_export' ) ); add_action( 'admin_post_rafgrid_merchant_feed', array( $this, 'merchant_feed' ) ); add_action( 'admin_post_nopriv_rafgrid_merchant_feed', array( $this, 'merchant_feed' ) ); add_filter( 'plugin_action_links_' . plugin_basename( RAFGRID_FILE ), array( $this, 'action_links' ) ); add_filter( 'plugin_row_meta', array( $this, 'row_meta' ), 10, 2 ); add_filter( 'admin_footer_text', array( $this, 'footer_text' ) ); } /** * Menu. */ public function menu() { $menu_title = (string) $this->settings->get( 'menu_title', '' ); if ( '' === $menu_title ) { $menu_title = 'Rafgrid'; } add_menu_page( $menu_title, $menu_title, 'read', 'rafgrid', array( $this, 'render_app' ), self::menu_icon(), 30 ); add_submenu_page( 'rafgrid', __( 'Sheets', 'rafgrid' ), __( 'Sheets', 'rafgrid' ), 'read', 'rafgrid', array( $this, 'render_app' ) ); add_submenu_page( 'rafgrid', __( 'Documentation', 'rafgrid' ), __( 'Documentation', 'rafgrid' ), 'read', 'rafgrid-docs', array( $this, 'render_docs' ) ); add_submenu_page( 'rafgrid', __( 'Settings', 'rafgrid' ), __( 'Settings', 'rafgrid' ), 'manage_options', 'rafgrid-settings', array( $this, 'render_settings' ) ); } /** * Admin menu icon: the Rafgrid "R made of cells" mark as a monochrome SVG. * WordPress expects a base64 data URI here and recolors it to match the * admin color scheme. * * @return string */ private static function menu_icon(): string { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- add_menu_page() requires a base64 SVG data URI; the SVG source is right above. } /** * Assets for the app screen only. * * @param string $hook Hook. */ public function assets( $hook ) { if ( false !== strpos( $hook, 'rafgrid-settings' ) || false !== strpos( $hook, 'rafgrid-docs' ) ) { wp_enqueue_style( 'rafgrid-fallback', RAFGRID_URL . 'assets/css/admin.css', array(), RAFGRID_VERSION ); if ( false !== strpos( $hook, 'rafgrid-settings' ) ) { wp_enqueue_script( 'rafgrid-settings', RAFGRID_URL . 'assets/js/settings.js', array(), RAFGRID_VERSION, array( 'in_footer' => true ) ); wp_localize_script( 'rafgrid-settings', 'RAFGRID_SETTINGS', array( 'copiedText' => __( 'Copied!', 'rafgrid' ), ) ); } if ( false !== strpos( $hook, 'rafgrid-docs' ) ) { wp_enqueue_script( 'rafgrid-docs', RAFGRID_URL . 'assets/js/docs.js', array(), RAFGRID_VERSION, array( 'in_footer' => true ) ); wp_localize_script( 'rafgrid-docs', 'RAFGRID_DOCS_I18N', array( /* translators: %d: number of search matches in documentation */ 'found' => __( '%d matches', 'rafgrid' ), 'sections' => __( 'sections', 'rafgrid' ), 'none' => __( 'Nothing found. Try different words.', 'rafgrid' ), ) ); } return; } if ( 'toplevel_page_rafgrid' !== $hook ) { return; } if ( ! Access::can_use( $this->settings ) ) { return; } $asset_file = RAFGRID_DIR . 'build/index.asset.php'; $asset = file_exists( $asset_file ) ? require $asset_file : array( 'dependencies' => array( 'wp-element', 'wp-i18n', 'wp-api-fetch', 'wp-components' ), 'version' => RAFGRID_VERSION, ); wp_register_script( 'rafgrid-app', RAFGRID_URL . 'build/index.js', $asset['dependencies'], $asset['version'], array( 'in_footer' => true ) ); wp_register_style( 'rafgrid-app', RAFGRID_URL . 'build/style-index.css', array( 'wp-components' ), $asset['version'] ); // Fallback CSS when the React build is missing (dev without npm). wp_register_style( 'rafgrid-fallback', RAFGRID_URL . 'assets/css/admin.css', array(), RAFGRID_VERSION ); wp_enqueue_media(); if ( file_exists( RAFGRID_DIR . 'build/index.js' ) ) { wp_enqueue_script( 'rafgrid-app' ); wp_enqueue_style( 'rafgrid-app' ); wp_set_script_translations( 'rafgrid-app', 'rafgrid', RAFGRID_DIR . 'languages' ); wp_localize_script( 'rafgrid-app', 'RAFGRID_BOOT', array( 'restUrl' => esc_url_raw( rest_url( 'rafgrid/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'xlsxUrl' => esc_url_raw( admin_url( 'admin-post.php?action=rafgrid_export_xlsx' ) ), 'xlsxNonce' => wp_create_nonce( 'rafgrid_export_xlsx' ), 'labelsUrl' => esc_url_raw( admin_url( 'admin-post.php?action=rafgrid_labels' ) ), 'feedUrl' => esc_url_raw( admin_url( 'admin-post.php?action=rafgrid_merchant_feed&key=' . urlencode( (string) $this->settings->get( 'merchant_feed_key', '' ) ) ) ), ) ); } else { wp_enqueue_style( 'rafgrid-fallback' ); } } /** * App root. */ public function render_app() { if ( ! Access::can_use( $this->settings ) ) { wp_die( esc_html__( 'You do not have permission to use Rafgrid.', 'rafgrid' ) ); } echo '
'; if ( ! file_exists( RAFGRID_DIR . 'build/index.js' ) ) { echo '

Rafgrid

'; echo esc_html__( 'The interface bundle is not built yet. Run "npm install && npm run build" inside the plugin folder, then reload this page.', 'rafgrid' ); echo '

'; } echo '
'; } /** * XLSX download (admin-post, streams binary with headers). */ public function download_xlsx() { if ( ! Access::can_use( $this->settings ) || ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), 'rafgrid_export_xlsx' ) ) { wp_die( esc_html__( 'Forbidden.', 'rafgrid' ) ); } $type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : 'post'; if ( ! Access::can_edit_type( $type ) ) { wp_die( esc_html__( 'Forbidden.', 'rafgrid' ) ); } $bin = Export::to_xlsx( $type, array( 'per_page' => 500 ) ); if ( '' === $bin ) { wp_die( esc_html__( 'XLSX is not available on this server (ZipArchive missing).', 'rafgrid' ) ); } nocache_headers(); header( 'Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ); header( 'Content-Disposition: attachment; filename="sheet-' . $type . '.xlsx"' ); header( 'Content-Length: ' . strlen( $bin ) ); echo $bin; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit; } /** * Printable product labels (name, SKU, price). */ public function labels_page() { if ( ! Access::can_use( $this->settings ) || ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), 'rafgrid_export_xlsx' ) ) { wp_die( esc_html__( 'Forbidden.', 'rafgrid' ) ); } $ids = array_map( 'absint', explode( ',', isset( $_GET['ids'] ) ? sanitize_text_field( wp_unslash( $_GET['ids'] ) ) : '' ) ); wp_register_style( 'rafgrid-labels', RAFGRID_URL . 'assets/css/labels.css', array(), RAFGRID_VERSION ); nocache_headers(); header( 'Content-Type: text/html; charset=utf-8' ); echo '' . esc_html__( 'Product labels', 'rafgrid' ) . ''; wp_print_styles( 'rafgrid-labels' ); echo ''; echo '

' . esc_html__( 'Print this page (or save as PDF) for product labels.', 'rafgrid' ) . '

'; foreach ( $ids as $id ) { $post = get_post( $id ); if ( ! $post || 'product' !== $post->post_type || ! Access::can_edit_post( $id ) ) { continue; } $sku = get_post_meta( $id, '_sku', true ); $price = get_post_meta( $id, '_price', true ); echo '
' . esc_html( $post->post_title ) . '
' . esc_html( $sku ?: ( '#' . $id ) ) . '
' . esc_html( $price ) . '
'; } echo '
'; exit; } /** * Download the last scheduled export (Settings -> Scheduled export). * The file lives in private storage; administrators only. */ public function download_export() { if ( ! current_user_can( 'manage_options' ) || ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), 'rafgrid_download_export' ) ) { wp_die( esc_html__( 'Forbidden.', 'rafgrid' ) ); } $path = Jobs::last_export_path(); if ( null === $path ) { wp_die( esc_html__( 'No scheduled export file yet.', 'rafgrid' ) ); } Storage::send( $path, 'text/csv; charset=utf-8', 'rafgrid-export-' . gmdate( 'Y-m-d', (int) filemtime( $path ) ) . '.csv' ); } /** * Google Merchant / Meta product feed (key-protected). */ public function merchant_feed() { $settings = new Settings(); $key = (string) $settings->get( 'merchant_feed_key', '' ); // Public feed read by Google/Meta crawlers: authenticated by the secret key, a nonce is not possible here. $given = isset( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( '' === $key || ! hash_equals( $key, $given ) ) { status_header( 403 ); exit; } $products = function_exists( 'wc_get_products' ) ? wc_get_products( array( 'limit' => 500, 'status' => 'publish', 'return' => 'objects' ) ) : array(); header( 'Content-Type: application/rss+xml; charset=utf-8' ); echo '' . " "; echo ''; echo '' . esc_xml( get_bloginfo( 'name' ) ) . '' . esc_xml( home_url( '/' ) ) . 'Product feed'; foreach ( $products as $p ) { $img = wp_get_attachment_image_url( $p->get_image_id(), 'full' ); echo ''; echo '' . esc_xml( $p->get_sku() ?: $p->get_id() ) . ''; echo '' . esc_xml( $p->get_name() ) . ''; echo '' . esc_xml( get_permalink( $p->get_id() ) ) . ''; echo '' . esc_xml( wp_trim_words( wp_strip_all_tags( $p->get_description() ), 60 ) ) . ''; echo '' . esc_xml( $p->get_price() . ' ' . get_woocommerce_currency() ) . ''; echo '' . ( 'instock' === $p->get_stock_status() ? 'in_stock' : 'out_of_stock' ) . ''; if ( $img ) { echo '' . esc_xml( $img ) . ''; } $gtin = method_exists( $p, 'get_global_unique_id' ) ? $p->get_global_unique_id() : ''; if ( $gtin ) { echo '' . esc_xml( $gtin ) . ''; } echo 'new'; echo ''; } echo ''; exit; } /** * Documentation page (translated via the plugin text domain). */ public function render_docs() { if ( ! Access::can_use( $this->settings ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'rafgrid' ) ); } $sections = array( 'start' => __( 'Getting started', 'rafgrid' ), 'editing' => __( 'Editing cells', 'rafgrid' ), 'filters' => __( 'Search and filters', 'rafgrid' ), 'columns' => __( 'Columns and presets', 'rafgrid' ), 'bulk' => __( 'Bulk editing and formulas', 'rafgrid' ), 'staged' => __( 'Staged edits and undo', 'rafgrid' ), 'woo' => __( 'WooCommerce mode', 'rafgrid' ), 'variations' => __( 'Variations and attributes', 'rafgrid' ), 'import' => __( 'Import and export', 'rafgrid' ), 'automation' => __( 'Automation rules', 'rafgrid' ), 'background' => __( 'Background jobs and snapshots', 'rafgrid' ), 'media' => __( 'Terms, comments, users, orders', 'rafgrid' ), 'mcp' => __( 'MCP for AI assistants', 'rafgrid' ), 'cli' => __( 'WP-CLI', 'rafgrid' ), 'settings' => __( 'Settings reference', 'rafgrid' ), 'support' => __( 'Author & support', 'rafgrid' ), 'faq' => __( 'FAQ and troubleshooting', 'rafgrid' ), ); ?>

Plugin language).', 'rafgrid' ); ?>

new with word diff before anything is saved. Duplicate, trash, restore and permanent delete live in the same bar; products also get Labels printing, alt text filling, AI excerpts and image-by-URL import there.', 'rafgrid' ); ?>

Supplier feed URL, CSV with sku,price,stock) updates prices and stock by SKU and can also run daily; running it requires an administrator. The daily scheduled CSV export of the default list is stored as a private file (never publicly reachable) and downloaded from Settings -> Scheduled export. Configuration export/import (Views & history) moves presets, views, column layouts and rules between sites as one JSON file; site settings are included only when an administrator exports, and the AI API key is never exported.', 'rafgrid' ); ?>

, empty, contains) THEN set another column to a value, optionally notifying via the bulk webhook or email. Example: if Stock qty = 0 then set Status to draft. Only administrators create, change and run rules. Hourly runs act as the administrator who saved the rule and manual runs as the administrator who presses Run, so every change passes that person\'s permissions and the normal save validations and lands in the audit log. A rule saved by someone who is no longer an administrator is skipped until an administrator saves it again (this also applies once to rules saved before version 2.0).', 'rafgrid' ); ?>

MCP) exposes %d tools over Streamable HTTP at /wp-json/rafgrid/v1/mcp for Claude, Cursor and other AI clients: querying, cell and bulk edits, variations, terms, comments, orders, snapshots and more. It covers content and catalog work only: there are no tools for users, roles, plugins, themes or site settings, automation rules are managed in wp-admin, and the admin tool group (snapshots, feed sync, background jobs, running rules, AI) works only for administrators. Access control offers Full, Read-only and Custom modes, per-role gates, tool groups (read, write, destructive, admin), an individual tool deny-list, per-user rate limiting and an IP allowlist. Every tool call is logged in Settings. The AI can never exceed the permissions of the WordPress user it authenticates as.', 'rafgrid' ), count( MCP::tools() ) ); ?>

Profile -> Application Passwords, creates a password for their AI client and puts it into the client config as the header "Authorization: Basic BASE64(username:application-password)". The assistant then acts exactly as that WordPress user. Revoking the application password in the profile disconnects the client at once, and security plugins that guard logins and application passwords keep working as usual.', 'rafgrid' ); ?>

wp rafgrid types --user=admin · wp rafgrid bulk --type=product --column=_regular_price --mode=increase_percent --value=10 --ids=1,2,3 --user=admin · wp rafgrid export --type=post --user=admin

Plugin language without changing the rest of the admin; Follow site language uses the site or user language when it is one of these.', 'rafgrid' ); ?>

  • Website:
  • Email:
  • Telegram:
  • Instagram: @
  • YouTube:

AI. Without either, nothing is sent anywhere.', 'rafgrid' ); ?>

settings->all(); $roles = get_editable_roles(); ?>

  1. Profile -> Application Passwords, enter a name (for example "Claude") and click "Add New Application Password". WordPress shows the password once.', 'rafgrid' ); ?>

{
  "mcpServers": {
    "rafgrid": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "",
        "--header", "Authorization: Basic BASE64(username:application-password)"]
    }
  }
}

🌐
✉
✈ Telegram
📷 Instagram @
▶ YouTube

settings->save( $data ); set_transient( 'rafgrid_saved_' . get_current_user_id(), 1, MINUTE_IN_SECONDS ); wp_safe_redirect( menu_page_url( 'rafgrid-settings', false ) ); exit; } /** * Plugin list row meta: documentation + official support channels. * * @param array $links Links. * @param string $file Plugin file. * @return array */ public function row_meta( $links, $file ) { if ( plugin_basename( RAFGRID_FILE ) !== $file ) { return $links; } $urls = Author::urls(); $links[] = '' . esc_html__( 'Documentation', 'rafgrid' ) . ''; $links[] = 'Telegram'; $links[] = '' . esc_html__( 'Support email', 'rafgrid' ) . ''; return $links; } /** * Admin footer credit on plugin screens only. * * @param string $text Footer text. * @return string */ public function footer_text( $text ) { if ( ! Plugin::is_plugin_screen() ) { return $text; } $urls = Author::urls(); return sprintf( '%s %s · %s · Telegram %s · Instagram @%s · YouTube %s', esc_html__( 'Rafgrid by', 'rafgrid' ), esc_html( Author::NAME ), esc_url( $urls['email'] ), esc_html( Author::EMAIL ), esc_url( $urls['telegram'] ), esc_html( Author::TELEGRAM ), esc_url( $urls['instagram'] ), esc_html( Author::INSTAGRAM ), esc_url( $urls['youtube'] ), esc_html( Author::YOUTUBE ) ); } /** * Action links. * * @param array $links Links. * @return array */ public function action_links( $links ) { $links[] = '' . esc_html__( 'Settings', 'rafgrid' ) . ''; return $links; } }