=== Scayla Connect ===
Contributors: liorzabari
Tags: seo, woocommerce, ai, structured-data, redirects
Requires at least: 6.4
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Connector for Scayla: measures whether AI assistants name your store, then writes SEO, FAQ schema and redirects, verified by read-back.

== Description ==

Scayla Connect is not an SEO plugin. It is the site-side connector for [Scayla](https://scayla.co.il/), a cloud service that measures whether AI assistants actually name your store when a shopper asks what to buy, and then acts on what it measured.

The queries it measures are not generic keywords typed into a box. They are derived from a scan of your own catalogue — real product titles, product types and categories — which is why they reach the level of a specific product or category. When a gap is found, the service turns it into a change on your site through this plugin, then measures the same fixed query set again the following week.

Every SEO value written is read back from the source your site will actually serve before the write is reported as successful. That is why a separate connector exists. Since Yoast SEO 14, Yoast does not serve SEO meta from post meta; it serves it from its own `wp_yoast_indexable` table. A tool that calls `update_post_meta( $id, '_yoast_wpseo_title', ... )` gets a successful database write, a 200 response, and a front end that keeps serving the old title indefinitely. This plugin writes, reconciles the indexable, re-reads from the serving source, compares, and on any mismatch returns a hard `scayla_write_not_verified` error carrying the expected value, the actual value and which storage layer answered.

Yoast SEO and Rank Math — the two SEO plugins this connector writes through — both ship AI-visibility tracking of their own, and track it well, at brand level. Scayla is the other shape of the same problem: catalogue-derived queries, a write path back into the exact product or category that caused the gap, and verification on every write. Scayla is not the only vendor that both measures and writes back, and this plugin does not claim to be.

**Hebrew and RTL.** Redirect paths never pass through `sanitize_text_field()`, which strips every percent-encoded octet and would silently turn an encoded Hebrew slug into a live 301 to the wrong page; the plugin uses its own percent-safe sanitiser. FAQ content renders as a native `<details>` / `<summary>` accordion with no JavaScript and no stylesheet, so it inherits your theme's RTL direction. Hebrew content analysis is not claimed as a differentiator — Yoast and Rank Math both support Hebrew already.

**What the plugin does on your site**

* Pairs with your Scayla account using a one-time admin Application Password, then operates through a dedicated `scayla-connector` service user with a narrow, revocable token.
* Bridges SEO title and meta description writes to Yoast SEO or Rank Math, with mandatory read-back verification on every write. With no SEO plugin active it stores and renders those values itself.
* Renders FAQ structured data as a single FAQPage node merged into Yoast's schema graph, or into Rank Math's JSON-LD, or as its own script tag when neither is present. The three paths are mutually exclusive, so an FAQPage is never rendered twice.
* Writes category and tag descriptions through a dedicated endpoint, because WordPress core registers `wp_filter_kses` on `pre_term_description` unconditionally and silently strips `div`, `h2`, `p`, `details` and `summary` while returning a 200. The plugin does not detach core's filter; it runs its own allowlist and outranks core, so nothing else on the site loses kses protection.
* Manages a redirects table for dead URLs, with guards against self-redirects, two-step loops, cross-host targets, and the protected prefixes `/wp-admin`, `/wp-json`, `/wp-login.php`, `/wp-content` and the site root.
* Adds one admin page: connection status, detected SEO plugin, redirect and FAQ counts, and a Disconnect button.

**How it is built.** Plain, readable PHP: six classes plus the main file and `uninstall.php`. No build step, no minified or obfuscated code, no bundled JavaScript, and no script, stylesheet, font or image loaded from a remote server. Nothing is compiled, so what you read in the plugin folder is exactly what runs.

**Nothing in the plugin is gated.** No premium tier inside this code, no license check, no feature flag, no nag, no upsell that unlocks a function. Every capability above works in full without paying anything. Paid limits exist only in the Scayla cloud service, and they limit how much work the service does, not what the plugin may do.

== External services ==

This plugin is a connector for Scayla, a third-party cloud service operated by Scayla (https://scayla.co.il/). **A Scayla account is required.** Without one the plugin has nothing to talk to: it will pair with nothing, receive no writes, and do nothing useful.

**The plugin makes no outbound requests.** It contains no `wp_remote_*`, `curl_*`, `file_get_contents` or `fsockopen` call anywhere in its code. It never contacts Scayla, on any schedule or on any event, including deactivation. All traffic is inbound: Scayla's servers call your site's REST API and authenticate with the paired token, and data leaves your site only inside the response to one of those authenticated requests.

**What leaves your site, and when:**

* **When you pair** (you start this from the Scayla portal; it arrives as an authenticated `POST /wp-json/scayla/v1/pair`): your site address and REST API address, your WordPress and WooCommerce version numbers, your permalink structure, which SEO plugin was detected, the login name of the service user just created, and — once only, in this one response — the API token your site mints for Scayla to authenticate with from then on. Your site keeps only a SHA-256 hash of that token.
* **While the service operates** (each time Scayla's servers call your site with the paired token): the content being worked on — product, post, page and category titles and descriptions, SEO titles and meta descriptions, image alt text, FAQ questions and answers, and redirect paths. Scayla reads these to know the current state, and writes new values back the same way.
* **When Scayla checks the connection** (`GET /wp-json/scayla/v1/health`, authenticated): plugin version, WordPress and WooCommerce versions, detected SEO plugin, permalink structure, site and home URL, locale, time zone, and the first 8 characters of the stored token hash as a connection fingerprint. The token itself is never returned.

**What never leaves your site:** no visitor data, no IP addresses, no analytics, no tracking beacons, no order or customer data, no user accounts or email addresses, and no data of any kind sent to any host other than the Scayla service that authenticated the request.

By pairing your site you agree to Scayla's terms and privacy policy:

* Terms of service: https://scayla.co.il/terms
* Privacy policy: https://scayla.co.il/privacy

== Installation ==

1. Upload the plugin files to `/wp-content/plugins/scayla-connect`, or install it through the WordPress Plugins screen.
2. Activate the plugin. Activation creates the redirects table and nothing else — no user account is created and no token exists yet.
3. Open the **Scayla** admin menu and read the external-service disclosure shown there before you continue.
4. Create an Application Password for your administrator account (Users → Profile → Application Passwords) and paste it into the Scayla portal to pair.
5. After pairing succeeds, revoke that Application Password. Scayla now holds its own limited token and no longer needs it.

== Frequently Asked Questions ==

= What exactly can the service do on my site? =

Pairing creates one user, `scayla-connector`, holding a custom role with exactly 13 capabilities: `read`, `edit_posts`, `edit_others_posts`, `publish_posts`, `edit_published_posts`, `delete_posts`, `delete_others_posts`, `delete_published_posts`, `edit_pages`, `edit_others_pages`, `edit_published_pages`, `upload_files`, `manage_categories`. With WooCommerce active, 9 product capabilities are added, including `read_private_products` (WooCommerce maps a product collection read to it) and `manage_product_terms`.

It does not get `manage_options`, `install_plugins`, `install_themes`, `switch_themes`, `edit_theme_options`, `edit_users`, `unfiltered_html`, `publish_pages` or `delete_pages`. Pages are editable but not publishable or deletable, because the service only writes SEO onto pages that already exist. The role is re-asserted on every load and any capability not on the list is stripped, so another plugin cannot quietly widen it.

= Is any code downloaded or executed from the remote service? =

No. The plugin never fetches, stores or evaluates remote code — no `eval`, no dynamic include, no remote file download. What arrives from Scayla is text: titles, descriptions, FAQ questions and answers, and paths. Each passes an explicit filter before storage: FAQ questions have all tags stripped and answers pass `wp_kses_post`; category and tag descriptions pass a post-context `wp_kses` allowlist extended only with `details`, `summary` and a small set of `class`/`id`/`data-` attributes, so `script`, `iframe` and `on*` handlers cannot be stored even if the token were stolen. Redirect paths pointing at another host are rejected outright.

= What happens if a write does not land? =

It fails loudly. After every SEO write the plugin reconciles the provider's storage, re-reads the value from the source your front end will actually serve, and compares. If they differ it returns HTTP 500 with `scayla_write_not_verified` and a body containing the expected value, the actual value, which storage layer answered (`indexable`, `postmeta`, `option` or `scayla`) and a hint distinguishing "Yoast's indexable did not reconcile" from "another plugin may be overriding SEO meta". For Yoast terms the read-back deliberately reads the indexable rather than the option just written, because a verification that reads back its own write proves nothing.

= Does it work without WooCommerce, or without an SEO plugin? =

Both are optional. Without WooCommerce it works on posts, pages, categories and tags; with it, products and product categories are covered too. Without Yoast or Rank Math it stores values in its own `_scayla_seo_title` and `_scayla_seo_description` meta and renders them through `pre_get_document_title` and a `wp_head` meta description. Those standalone renderers refuse to run unless the detected provider is exactly "none", so there is never a duplicate title alongside Yoast or Rank Math.

= Does it slow down my site? =

The redirect matcher runs at `template_redirect` and costs zero database queries when a request does not match: the map lives in one autoloaded option WordPress has already fetched, an empty map returns before the request URI is parsed, and only a hit touches the database. If the map would exceed 100 KB it is dropped in favour of a single prepared query, so a large table never bloats every admin and cron request. Paths are stored in `utf8mb4_bin` columns so matching is byte-exact, which stops a case-normalising rule such as `/Page` to `/page` from matching its own destination and looping forever. The plugin registers no cron jobs.

= Can the service user log in? =

No. It is created with a 64-character random password that is never transmitted, displayed, stored in plaintext or logged — but the guarantee does not rest on that. An `authenticate` filter rejects any interactive login resolving to the service user, and an `allow_password_reset` filter refuses password resets for it, closing the otherwise real path of requesting a reset to `connector@yourdomain` on a catch-all mail domain. The token maps to the service user only on REST requests, anchored to the first path segment via `rest_get_url_prefix()`, so a front-end permalink merely containing `wp-json` cannot borrow the identity.

= Are any of the REST routes public? =

No. Every route has a real permission callback, including `GET /health`; there is no `__return_true` anywhere in the code. Write routes additionally check the specific object: `current_user_can( 'edit_post', $id )` for that post, or term existence plus a taxonomy allowlist plus `manage_categories` for that term. The bulk read endpoint enforces authorisation per ID rather than once per batch, and anything the service user may not touch is returned in a `skipped` list instead of being read. Failed token attempts are rate-limited per IP, bucketed on `REMOTE_ADDR` rather than on spoofable proxy headers.

= How do I disconnect, and what is removed if I delete the plugin? =

Click **Disconnect** on the Scayla admin page. The token hash is deleted immediately and every subsequent request from Scayla is rejected; your content, SEO values, FAQ data and redirects stay exactly as they are. Deactivating changes nothing else, so you can deactivate to troubleshoot and reactivate without re-pairing.

Deleting the plugin removes the role from the service user, deletes the role, drops the redirects table, deletes `_scayla_faq`, `_scayla_seo_title` and `_scayla_seo_description` from every post and term, and sweeps every `scayla_*` option and transient — on every site of a multisite network. Two things are kept on purpose: the `scayla-connector` account, so content it authored keeps its author attribution (it has no role, zero capabilities and cannot be logged into; delete it manually if you prefer), and SEO values already written into Yoast or Rank Math, because at that point they are your site's meta, not the plugin's.

= Does it support multisite? =

Yes. Network activation provisions every existing site, sites created afterwards are provisioned automatically, and uninstall walks every site so no orphan table or role is left behind.

== Changelog ==

= 1.0.0 =
* First public release.
* Pairing through a one-time admin Application Password; day-to-day access through a scoped, revocable token stored only as a SHA-256 hash.
* Dedicated service user and role with a fixed capability list that cannot be logged into or password-reset.
* SEO title and meta description bridge for Yoast SEO and Rank Math, plus a standalone mode, with read-back verification on every write.
* FAQPage JSON-LD merged into Yoast's schema graph or Rank Math's JSON-LD, with a self-contained fallback.
* Category and tag description endpoint that preserves structural HTML through an allowlist without detaching core's kses filter, and verifies the stored result.
* Redirects table with byte-exact matching, loop, self-redirect, cross-host and protected-path guards, and a zero-query front-end match on a miss.
* Multisite provisioning and multisite-aware uninstall.

== Upgrade Notice ==

= 1.0.0 =
First public release. No upgrade steps.
