The service side lives in the
patternbuilderwp.com repository;
this is the plugin half. Collections are collections.md,
dependency trees and attribution are dependencies.md.
Users connect their account per WordPress user by signing in or signing up
inside wp-admin. The connect panel posts credentials to this site's proxy,
which relays them server-side to the service's /auth/login or /auth/signup
and stores only the returned bearer token (Pattern_Builder_Cloud, user meta).
The browser never visits the service, and credentials are never logged or
stored.
The service URL is the pattern_builder_cloud_url option, overridable by a
PATTERN_BUILDER_CLOUD_URL wp-config constant — the declarative choice for dev
setups, since it survives DB resets — and filterable.
The browser never talks to the service. All cloud traffic flows through
nonce- and capability-gated proxy routes (/pattern-builder/v1/cloud/*,
Pattern_Builder_Cloud_Controller), which relay every refusal as it came,
upgrade link included: the service decides, the plugin only says so.
Both cloud tabs show the connect panel until a token exists, and the proxy
enforces the same rule — a disconnected /cloud/directory, /cloud/collections
or directory /cloud/download answers 401. So what a site downloads is
downloaded by somebody. The website's own directory stays public.
Pattern_Builder_Cloud_Porter converts local patterns ↔ the service's Portable
Pattern Package (pbp/1).
Exports bundle local images as pbp-asset:// placeholders plus files.
Every reference the service checks is matched by host and path — src, a block
attribute's url, CSS url() — so a query string or an http/https difference
does not hide one, since anything left pointing at this site is refused there.
An image on another host, or one of a type a package cannot carry (JPEG, PNG,
GIF, WebP only), fails the export with that URL named. Attachment ids and
wp-image-N classes are dropped on the way out, since id 57 names nothing on
another site.
Imports fetch package assets into the media library, re-sanitize the markup
(KSES and scheme checks — never trust the wire), then land as a wp_block or
flow through Pattern_File_Store::update_theme_pattern(). A user pattern's
blocks are pointed at the attachments just created; a theme pattern's images
move into the theme's own assets/images, so it carries no ids. Assets are
always fetched from the configured service origin — URLs are re-rooted onto it,
never fetched from the host they name — so a service self-identifying by a
different URL than the one this site reaches it by just works.
A package says which WordPress it needs (minWordPress), and
version_problem() refuses an install this site is too old for before
anything is written. The re-sanitize runs against this site's KSES, so an
older release does not merely fail to render unknown markup — it strips it, and
the pattern would land looking installed and missing what it was for. MathML is
the case that costs content rather than appearance. The browser says so first,
from needsNewerWordPress(); the server is the check that counts.
Design tokens. A download carrying tokens this site lacks shows them first, then writes only the missing ones into the destination the user already chose: theme.json for a theme pattern, Global Styles for a user one. The server re-checks.
Block style variations, for the same reason and by the same route: the
markup carries is-style-{slug} and the definition lives in the theme it was
authored in, so a pattern reaching for one would otherwise arrive with the
class intact and nothing styling it. Block_Style_Variations::carried_by()
gathers what the markup applies — only what this site defines, since one
declared in a block's own block.json ships with WordPress and would collide
with core's — and install() writes the ones the destination lacks, never
overwriting, always into the theme. Global Styles has no partials mechanism to
register one in, which is the same reason a dependency always lands as a theme
pattern.
collect_tree() therefore walks one level further down than the markup:
markup → is-style-* → the definition → the presets that references, because
a variation's colours are in its definition and none of them appear in the
markup.
Global styles deliberately do not travel. There is one
styles.elements.link.color.text on a site and it paints every page, so a
pattern that repainted its destination on install would be vandalism.
Nor does additionalMetadata. What a pattern file's header comment carries
beyond its headers describes the theme it was written in — @package and its
neighbours — and would be a false claim about the theme it lands in. The
pbp/1 payload is an explicit list of fields, so it never left in the first
place.
WordPress does not sanitize a theme.json css property — it gates it on
edit_css — so a string closing its own selector writes rules for the whole
document. set-global-styles therefore refuses one outright: a css at the
root or on an element is scoped to nothing a pattern brought with it.
A variation is the exception. add-block-style-variation accepts a css at the
partial's top-level styles.css and nowhere deeper, because its selector is a
class the pattern's own markup carries — and without one a variation cannot
express a pseudo-element, a descendant rule or a hover state, which is most of
what a variation is for.
What it accepts is decided by Safe_Css, a grammar rather than a filter:
declarations first, then nested rules anchored on &. That is a strict subset
of what WP_Theme_JSON::process_blocks_custom_css() parses correctly — that
parser splits on &, strips every } and explodes on {, so a declaration
after a rule, a second level of nesting and a & inside a string are all
refused because core mis-reads them.
The bans that are about danger:
- No
@and no comments. - No
<anywhere, quoted strings included: the HTML tokenizer ends a<style>element at</stylewhatever CSS thinks a string is. - No
,,+or~in a selector: core'sscope_selector()distributes over a comma and landsbodyat the top level, and a sibling combinator reaches outside the block the markup carries. - No backslash outside a quoted string, where
u\72l(spellsurl(past a name check — and allowed inside one, wherecontent: "\2713"is how an icon glyph is written. - An allow list for every
name(in a value.
Nothing is stripped or repaired: a repair is how a checker and a browser come to disagree. A refusal names the rule and the fragment.
The check runs three times — author, service, destination — and the third
is the one that counts, because it is the machine that will execute the CSS. A
string refused there costs that one look rather than the download
(variationsRefused).
There is deliberately no edit_css gate: a gate would say unvalidated CSS
is acceptable from a privileged caller, and edit_css is super-admin-only on
multisite, so it would stop an ordinary network administrator writing what they
can already write by hand.
includes/class-safe-css.php is vendored byte-for-byte identical but for its
namespace into the service as includes/patterns/class-safe-css.php; a
disagreement between the copies is the vulnerability. The accept/reject corpus
in tests/php/fixtures/safe-css-cases.json is the same file in both
repositories, so a rule loosened on one side turns a test red on the other.
The link lives on the pattern itself: the Cloud: file header, or
pattern_builder_cloud post meta for a user pattern —
{handle}/{collection}/{slug}, written by an upload (the name the service
answers with) and by an install (the name the package carries), carried through
theme↔user conversion, a rename and update-pattern, and dropped by
duplication.
It is a separate field from Origin: because a pattern can be somebody else's
work and have a copy of its own; overwriting attribution with a
self-reference would lose the credit on the next update.
Nothing about the relationship is remembered. Whether the copy still
exists, and whether it is the connected account's, is asked of the service each
time (Pattern_Builder_Cloud::own_pattern()), so a pattern deleted on the
cloud, a deleted collection or a different account connected here needs no
bookkeeping and simply reads as not on the cloud.
The download side needs no remembered reference either:
Pattern_File_Store::cloud_names() lists every cloud name a local pattern
answers to, from file headers and one meta query. That is what the details
card's Edit button, the collection view's installed markers, the tiles'
"installed n of m" and a whole-collection install's skip all read.
Connected users get PatternCloudControls inside the Pattern Source panel, so
it appears in both the browse sidebar and the editor:
- Update pattern on the cloud and Delete from cloud when
/cloud/pattern-statefinds the pattern's reference in the connected account's library, with the line above them naming its collection. - Upload to the cloud with the collection picker otherwise — never uploaded, somebody else's, or a copy since deleted.
There is no "changed since upload": Update is always offered, and pushing one that changed nothing costs a request. Deleting from the panel also clears that pattern's reference, so a different pattern uploaded under the name later is not taken for its copy.
Three things the plugin relays rather than decides.
Creating an account applies the service's password rule (eight characters
with an upper-case letter, a digit and a symbol — passwordProblem() mirrors
it so the form says what is missing before the round trip), asks the marketing
question as two buttons with neither preselected (a pre-checked box is not
consent under GDPR and reads as opt-out to the wp.org review team; no answer
relays as no), links the service's Terms of Service and Privacy Policy above
the button (at the configured service's own /terms/ and /privacy/, so a
development service shows its own), and leaves the account unverified until
the emailed link is opened. Forgot your password? posts the address to
/cloud/password/forgot; the emailed link finishes the reset on
patternbuilderwp.com, never here.
Go Pro opens Freemius's overlay checkout on the Pattern Builder screen
(src/cloud/checkout.js). The service's /me carries the configuration;
Freemius's script loads on the first click and never before — it is the one
third-party script this plugin loads, permitted by wp.org guideline 8 as a
documented service on its own domain, and the readme's External services
section says so. purchaseCompleted posts the licence id to
/cloud/billing/sync so Pro is on before the overlay closes; the hosted
checkout URL stays as the fallback and the status poll as the safety net.
Usage telemetry is opt-in, because wp.org guideline 7 requires explicit
consent. The browse app asks once per site with Allow and No thanks
(TelemetryPrompt), a site that declined is offered it once more as a one-line
Allow on the connect panel, and track() is a no-op until the answer is yes.
When allowed, named events carry the environment — plugin, WordPress and PHP
versions, locale, theme slug, multisite, environment type, and the connected
account id — under a random install id minted at opt-in. Never the site's URL
or name, never content. Events are buffered per request and posted once on
shutdown to the service's public /telemetry relay, non-blocking; a lost
batch is lost. The plugin loads no analytics script and names one service.
Neither the plugin nor the service runs a model; the service carried a pipeline
for a while and has removed it (its decision log, D47). Generation is what an
agent the user already runs does through the abilities — create, edit, upload,
install, read the authoring guide — with the pattern-author and
design-reproduction skills, on the user's own provider. The create-pattern
modal makes blank patterns only, and there is no generate proxy.
| Piece | Job |
|---|---|
Pattern_Builder_Cloud |
Token store, account cache, own_pattern(). |
Pattern_Builder_Cloud_Controller |
The proxy routes. |
Pattern_Builder_Cloud_Porter |
pbp/1 conversion both ways, trees, installs. |
Pattern_Builder_Cloud_Tokens |
Design tokens: the value grammar and the never-overwrite rule. |
Pattern_Builder_Cloud_Abilities |
The seven cloud abilities. |
Pattern_Builder_Telemetry |
The site's answer, the install id, the shutdown batch. |
src/cloud/ |
CloudBrowser (shell), CommunityTab, UploadedTab, CollectionTile, CollectionView, SaveCollectionFlow, CollectionPicker, collections (pure helpers), checkout. |
The plugin remains fully functional disconnected.