=== VendMizer ===
Contributors: vendmizer
Tags: ecommerce, online-store, shop, payments, store
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.65.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

VendMizer is the complete commerce platform for WordPress. Launch a professional online store with a ready-to-use, customizable storefront.

== Description ==

VendMizer is the complete commerce platform for WordPress. Launch a professional online store with a ready-to-use, customizable storefront and the business tools behind it. Manage products, orders, payments, shipping, inventory, customers and sales analytics from one platform — no extra theme, no design experience and no plugin juggling.

This plugin is fully functional on its own. It includes:

* Product catalog with categories, attributes, brands and product tags
* Cart, a complete checkout flow, order management and a customer directory
* Coupons and discounts, inventory tracking, shipping rates and tax rates
* Reviews, wishlist and digital downloads
* A built-in storefront with editable Home, About and Contact pages, a blog, and scheduled announcement banners you can show as a top bar, a popup, a slide-in or inline
* Analytics dashboard with revenue and order reporting
* A privacy-first cookie consent banner that holds Google Analytics and the Meta Pixel back until the visitor agrees (granular categories, bilingual, first-party only, no IP lookup)
* Contact inbox, media library, staff roles and outgoing webhooks
* Maintenance and coming-soon modes, with an optional countdown, a bypass link for you, and an optional visitor email signup
* Multilingual interface — English and Arabic out of the box, with full right-to-left support, and you can add and translate further languages

VendMizer integrates with third-party payment, shipping and anti-spam services. They are optional and contact nothing until you configure and enable them; "External services" below lists every one and what it sends.

VendMizer is a whole storefront and admin rather than a widget, so it takes over things a smaller plugin leaves alone: your login page, where staff land, and some URLs. The FAQ describes each, and how to switch it off.

A separate commercial extension adds modules of its own. It is not required, nothing here is locked or degraded by its absence, and this plugin never advertises it to you in the admin.

== External services ==

VendMizer can connect to the third-party services below. With one exception, none is contacted unless you enable and configure the matching feature, and no data reaches any of them while that feature is off. The exception is Gravatar, at the end.

Two admin actions also reach a service, and only when you click them. "Test connection" on a payment gateway, shipping carrier or SMTP settings screen sends the credentials you entered to that provider to confirm they work (the Payoneer test additionally sends your site address and a one-dollar sample order that is never charged), and "Send test email" relays a test message through the SMTP host you configured. Neither runs on its own.

**Payment gateways.** When you enable a gateway and a customer pays with it, order and payment details (amount, currency, order reference and the billing information needed to process the charge) go to that provider to create and confirm the transaction. Supported providers and their terms/privacy policies:

* Stripe (API host: api.stripe.com) — https://stripe.com/legal/ssa — https://stripe.com/privacy
* PayPal (API hosts: api-m.paypal.com, and api-m.sandbox.paypal.com in sandbox mode) — https://www.paypal.com/us/legalhub/useragreement-full — https://www.paypal.com/us/legalhub/paypal/privacy-full
* Square (API hosts: connect.squareup.com, and connect.squareupsandbox.com in sandbox mode) — https://squareup.com/us/en/legal/general/ua — https://squareup.com/us/en/legal/general/privacy
* Authorize.Net (API hosts: api.authorize.net, and apitest.authorize.net in test mode) — https://www.authorize.net/about-us/terms.html — https://www.visa.com/en-us/legal/global-privacy-notice
* 2Checkout (Verifone) (API hosts: api.2checkout.com, and api.avangate.com in sandbox mode) — https://www.2checkout.com/legal/terms/ — https://www.2checkout.com/legal/privacy/
* Klarna (API host depends on the region of your Klarna account: api.klarna.com, api-na.klarna.com or api-oc.klarna.com, and api.playground.klarna.com, api-na.playground.klarna.com or api-oc.playground.klarna.com in sandbox mode) — https://www.klarna.com/international/terms-and-conditions/ — https://www.klarna.com/international/privacy-policy/
* Moyasar (API host: api.moyasar.com) — https://moyasar.com/en/resources/terms/ — https://moyasar.com/en/resources/privacy-policy/
* Tamara (API hosts: api.tamara.co, and api-sandbox.tamara.co in sandbox mode) — https://tamara.co/en-sa/terms-and-conditions — https://tamara.co/en-sa/privacy-policy
* Tabby (API host: api.tabby.ai) — https://tabby.ai/en-AE/legal — https://tabby.ai/en-AE/legal/privacy-policy/latest
* Payoneer Checkout, the Oscato platform (hosted payment page; API hosts: api.live.oscato.com, and api.sandbox.oscato.com in sandbox mode) — https://www.payoneer.com/legal/ — https://www.payoneer.com/legal/privacy-policy/

Some gateways complete the payment on a page the provider hosts rather than on your site: PayPal, Klarna, Tamara, Tabby and Payoneer send the customer's browser to that provider's checkout page to approve the payment, and 2Checkout may send it to the card issuer's 3-D Secure page; the customer is returned to your store afterwards. Those pages are the provider's own, at addresses the provider supplies in its API response.

Several of these gateways collect card details in a field the provider hosts, keeping card numbers off your server. That field is drawn by the provider's own JavaScript, so enabling one loads a script from js.stripe.com (Stripe), web.squareup.com or sandbox.web.squareup.com (Square), js.authorize.net or jstest.authorize.net (Authorize.Net), 2pay-js.2checkout.com (2Checkout), or cdn.moyasar.com (Moyasar, which also supplies a stylesheet). Loading these tells the provider the visitor's IP address and user agent on any page carrying the checkout, before the visitor types anything; Stripe's and Moyasar's also load in the customer account area when a shopper retries payment on an unpaid order. They must come from the provider's own origin.

**Shipping carriers.** When you enable a carrier and request a rate or create a shipment, the recipient address, parcel weight and dimensions and your sender details go to that carrier to generate rates, labels and tracking:

* Aramex (API hosts: ws.aramex.net, and ws.dev.aramex.net in test mode) — https://www.aramex.com/us/en/terms-of-use — https://www.aramex.com/us/en/legal-details/privacy-policy
* SMSA Express (API hosts: track.smsaexpress.com, and stg-track.smsaexpress.com in test mode) — https://www.smsaexpress.com/terms-and-conditions — https://www.smsaexpress.com/privacy-policy
* Naqel Express (API host: infotrack.naqelexpress.com in test mode; in live mode the endpoint URL is the merchant-specific one you enter in the carrier settings) — https://www.naqelexpress.com/en/About/TermsCondition — https://www.naqelexpress.com/en/privacypolicy/cookies-policy/
* USPS (API hosts: api.usps.com, and api-cat.usps.com in test mode) — https://about.usps.com/who/legal/terms-of-use.htm — https://about.usps.com/who/legal/privacy-policy/full-privacy-policy.htm
* EasyPost (multi-carrier rating and label generation; API host: api.easypost.com) — https://legal.easypost.com/ — https://legal.easypost.com/#privacy-policy
* Shippo (multi-carrier rating and label generation; API host: api.goshippo.com) — https://privacy.goshippo.com/policies?name=terms-of-use — https://privacy.goshippo.com/policies?name=privacy-notice

When a carrier returns a label as a URL rather than file data, the plugin can download that PDF once so it can be reprinted. The fetch is off unless you enable it, and the address is the carrier's own domain or file-delivery CDN, so it cannot be listed in advance. Nothing is sent beyond the URL the carrier supplied; cleartext http:// addresses and any address that does not resolve to a public host are refused.

**Spam protection.** If you enable a CAPTCHA provider, the visitor's challenge token and IP address — plus the secret key you entered — go to that provider when a protected form is submitted. Verification hosts: www.google.com (reCAPTCHA), api.hcaptcha.com (hCaptcha), challenges.cloudflare.com (Turnstile). The widget script loads separately in the visitor's browser from www.google.com, js.hcaptcha.com or challenges.cloudflare.com on any page carrying a protected form:

* Google reCAPTCHA — https://policies.google.com/terms — https://policies.google.com/privacy
* Cloudflare Turnstile — https://www.cloudflare.com/website-terms/ — https://www.cloudflare.com/privacypolicy/
* hCaptcha — https://www.hcaptcha.com/terms — https://www.hcaptcha.com/privacy

**Currency exchange rates.** When you change your store's base currency or enable an additional one, the plugin fetches reference rates from Frankfurter, which sources European Central Bank data. Only currency codes are sent. The API host is api.frankfurter.dev, and the plugin does not follow redirects on that request, so it cannot be carried to a host not named here. Frankfurter publishes no separate privacy policy; its terms are its licence and its data-handling statement is on its home page. Service and privacy statement: https://frankfurter.dev/ — Licence/terms: https://github.com/lineofflight/frankfurter/blob/main/LICENSE

**Analytics and advertising.** If you enter a Google Analytics 4 / Google Tag Manager measurement ID or a Facebook (Meta) Pixel ID in the settings, the storefront can load that provider's script — Google's from www.googletagmanager.com, Meta's from connect.facebook.net — and send visitor and e-commerce events (page views, product views, add-to-cart, checkout, purchases). Both are off until you enter an ID, and an ID alone does not start them: the storefront holds both scripts behind a consent banner until the visitor grants the matching category, with Google Consent Mode v2 default-deny signals set first. No request reaches either host before the grant. The gate covers these two scripts only; every other embed listed here loads under its own entry.

* Google Analytics / Google Tag Manager — https://policies.google.com/terms — https://policies.google.com/privacy
* Facebook (Meta) Pixel — https://www.facebook.com/terms/ — https://www.facebook.com/privacy/policy/

**Maps and address lookup.** If you enable the map address picker, the storefront loads map tiles from OpenStreetMap (tile.openstreetmap.org) and uses OpenStreetMap's Nominatim service (nominatim.openstreetmap.org) to geocode the address a customer types and, when the customer drops the pin instead, to turn the map coordinates they chose back into an address; the customer's language preference is sent with the lookup so the result comes back in it. The picker appears at checkout, in the customer account address book, and in the admin for shipping zones and your store location. Neither host is contacted until it is opened.

* OpenStreetMap (map tiles) — https://operations.osmfoundation.org/policies/tiles/ — https://osmfoundation.org/wiki/Privacy_Policy
* Nominatim (geocoding) — https://operations.osmfoundation.org/policies/nominatim/ — https://osmfoundation.org/wiki/Privacy_Policy

**Map embeds (Google Maps / OpenStreetMap / Bing Maps / Mapbox / Yandex Maps).** If you paste a map embed URL into your contact page, your About page or a homepage section, the storefront frames that provider's map on that public page. Only https addresses on these hosts are accepted: google.com, www.google.com, maps.google.com, openstreetmap.org, www.openstreetmap.org, bing.com, www.bing.com, api.mapbox.com, yandex.com and yandex.ru. Any other host is discarded on save, so the storefront cannot be made to frame an arbitrary site; the `vendm_map_embed_hosts` filter adds a regional provider. The plugin sends no customer data, but a visitor loading the page tells that provider their IP address and user agent, as any embedded frame does. https://policies.google.com/terms — https://policies.google.com/privacy — https://osmfoundation.org/wiki/Privacy_Policy — https://www.microsoft.com/en-us/servicesagreement — https://www.microsoft.com/en-us/privacy/privacystatement — https://www.mapbox.com/legal/tos — https://www.mapbox.com/legal/privacy — https://yandex.com/legal/rules/index.html — https://yandex.com/legal/confidential/en/

**Video embeds (YouTube / Vimeo).** If you add a YouTube or Vimeo video URL to a product, blog post or storefront section, the storefront embeds that provider's player on that public page. YouTube players are framed from www.youtube-nocookie.com — YouTube's privacy-enhanced host, so the view is not used to personalise the visitor's YouTube experience or the advertising they see elsewhere. Vimeo players are framed from player.vimeo.com. No thumbnail or other file is loaded from either provider: where a section shows a still image with a play button, that image is one you uploaded. This happens only for videos you add, and the plugin sends no customer data. https://www.youtube.com/t/terms — https://policies.google.com/privacy — https://vimeo.com/legal — https://vimeo.com/legal/privacy/policy

**Outgoing webhooks.** If you create a webhook, the plugin sends the event data you configure to the destination URL you specify. That destination is entirely under your control.

**Search engine indexing (IndexNow).** If you turn on IndexNow in the SEO settings, the plugin notifies that service when your store URLs change so participating search engines (such as Microsoft Bing) can re-crawl them. Only public URLs from your own site are sent. It is off by default and needs a key you generate in the settings. The API host is api.indexnow.org. IndexNow publishes no separate privacy policy; its terms document contains its privacy statement. Service: https://www.indexnow.org/ — Terms and privacy statement: https://www.indexnow.org/terms

**Outgoing email (SMTP).** If you configure an external SMTP server for store email, the plugin connects to the host you name and relays order confirmations, shipping notices and password resets through it, including the recipient's address and the message body. The host, the credentials and therefore the recipient of that data are your choice. Leave SMTP off and WordPress's own mail handling is used instead, with no external connection.

**Avatars (Gravatar).** The staff list, staff detail and your own profile screen in the VendMizer admin show each staff member's photo from Gravatar, operated by Automattic, because that is what WordPress itself uses. The request sends a one-way hash of the staff member's email address plus the viewing browser's IP address and user agent. The address is built by WordPress's own `get_avatar_url()`, so the host is whichever your WordPress version uses — secure.gravatar.com on current releases. No customer, order or store data is involved, and it happens only on those admin screens, never on your storefront. Turning off Settings → Discussion → "Show Avatars" stops it entirely; the plugin then draws coloured initials locally.

* Gravatar (Automattic) — https://wordpress.com/tos/ — https://automattic.com/privacy/

== Installation ==

1. Upload the `vendmizer` folder to the `/wp-content/plugins/` directory, or install the plugin through the WordPress Plugins screen.
2. Activate the plugin through the Plugins screen in WordPress.
3. Open VendMizer from the admin menu and follow the setup wizard, which walks you through your store name and timezone, your country, currency and units, then payments, shipping and which features to switch on.
4. Optionally configure payment gateways, shipping carriers, and other integrations under Settings. Each integration is off until you enable it.

== Frequently Asked Questions ==

= Is any feature here locked, limited, or switched off until I pay? =

No. There is no licence key, no activation, no registration and no trial clock. Nothing checks a date or an entitlement before letting you use a feature, and no part of the plugin stops working after any period. The plugin contacts no VendMizer server: the only vendmizer.com address in the code is the Plugin URI line in the plugin header, and nothing fetches it. All seventeen modules ship switched on, and Settings → Modules lets you turn any of them off and back on whenever you like.

A separate commercial extension exists and adds modules of its own. Nothing in this plugin is disabled, degraded or hidden by its absence, and this plugin never advertises it to you in the admin.

= Does VendMizer require another e-commerce plugin? =

No. VendMizer is fully standalone and runs on its own database tables and REST API.

= Does it change my WordPress login page? =

Yes. VendMizer replaces wp-login.php with its own login screen so that staff and customers sign in through the storefront rather than through WordPress. Anyone who visits wp-login.php is sent to the VendMizer login page instead, and password reset, registration and logout follow the same route.

If you would rather keep the standard WordPress login, add `define( 'VENDM_DISABLE_LOGIN_TAKEOVER', true );` to wp-config.php, or `add_filter( 'vendm_login_takeover_enabled', '__return_false' );` from a small plugin of your own. Either one steps VendMizer out of the way completely: wp-login.php works as it always did, and every login, logout, password-reset and registration link WordPress generates points back at it rather than at the storefront. If you are ever locked out — a broken theme, a caching problem, anything — visiting wp-login.php?vendm_native_login=1 always renders the stock WordPress form.

= Where do store staff land after signing in? =

Anyone you make store staff is sent from wp-admin to the VendMizer dashboard instead, because wp-admin has nothing in it for them. That covers the Shop Admin and Shop Manager roles VendMizer creates, and any existing WordPress user you give one of them to. Your WordPress administrators are never redirected, and neither are your store customers. The profile screen always stays reachable, so everyone can still change their own password, and editors, authors and subscribers who are not store staff carry on using wp-admin exactly as before. To turn the redirect off, use `add_filter( 'vendm_redirect_staff_from_wp_admin', '__return_false' );` — it receives the user's ID, so you can decide person by person.

= Which URLs does the storefront take over? =

The storefront is served from /shop/, the admin from /shop-admin/ and the customer account area from /shop-account/. All three are prefix rules that claim everything beneath them, so a page of your own at, say, /shop-account/anything is no longer reachable. The admin and account paths are fixed; the storefront path is held in the `vendm_store_slug` option, which has no settings screen of its own in this release, so changing it means changing the option directly. If you switch on "Use the site home page for the store" under Settings → Storefront, the storefront is served from your site root instead and your existing WordPress front page is no longer shown — that option is off unless you choose it.

Four more routes are claimed under /vendm-auth/ — /vendm-auth/google/start, /vendm-auth/google/callback, and the same pair for facebook. They are the redirect endpoints a sign-in provider is registered against. This plugin implements no provider, so all four are inert here: the handler checks whether anything is listening and returns immediately when nothing is, which is the case unless you install an extension that adds one. Product images are also served through short /m/ URLs, described under "What does the plugin write to my uploads folder?" below.

Your product sitemap is always available at /vendm-sitemap.xml, and the plugin additionally answers the shorter /sitemap.xml when no other plugin has claimed that route. If you run Yoast, Rank Math, All in One SEO or similar, they keep it and VendMizer quietly uses its own prefixed URL, which is the one it advertises in robots.txt. There is also a product feed for Google Merchant Center and Meta Commerce at a REST endpoint.

VendMizer creates its own database tables rather than storing products as posts. Uninstalling gives you the choice of keeping your data or removing it completely, and the default is to keep it: deleting the plugin leaves your store intact unless you have turned that setting off first. If you do choose removal, every table, option, user meta key, role and scheduled task the plugin created is deleted.

= Are storefront pages rendered on the server? =

Yes, always. The home page, product, category, shop, brand, tag, blog and custom pages are all delivered as complete HTML from the server, so search engines and social previewers read real content and shoppers see the page before any JavaScript has run; the storefront app then takes over the page in place for the interactive experience. There is no setting for this — it is how the storefront works — and the cart, checkout, account and search screens, which are personal to each visitor, are rendered by the app on purpose.

If you ever need to switch server rendering off while diagnosing a problem on a live site, add `define( 'VENDM_DISABLE_SSR', true );` to wp-config.php, or `add_filter( 'vendm_storefront_ssr_enabled', '__return_false' );` from a small plugin of your own; every page then loads through the app instead. It is an emergency switch, not a preference: leaving it on costs you the search and first-paint benefits. To leave a single home-page section type to the app while keeping everything else server-rendered, return its type key from the `vendm_ssr_skip_section_types` filter.

= What does the plugin write to my uploads folder? =

Three directories inside wp-content/uploads, and nothing written there is copied to us or to any third party.

Files you sell go into `uploads/vendmizer-private/`, inside a subdirectory whose name is twenty random characters generated once for your site and stored in your database, and each file is saved under a random UUID name rather than the name it was uploaded with. At activation the plugin puts three guard files in that directory: an `.htaccess` that denies every request to it and switches off PHP execution and CGI handlers, a `web.config` that denies every request on IIS, and an empty `index.php` so the directory cannot be listed. Customers never touch any of that — a purchased file is delivered by PHP through a short-lived signed link that is checked against the order before a byte is sent. Read the next sentence carefully, because it is the part that depends on your host: `.htaccess` is an Apache mechanism and `web.config` an IIS one, so on nginx, Caddy or LiteSpeed in its non-Apache mode neither file is read at all, and the random directory name is then the only thing between the internet and a paid file. That name is unguessable rather than secret, and it is not an access check. If you run nginx and sell downloads, add a rule denying `/wp-content/uploads/vendmizer-private/` to your server config; it takes one block and it is the only way to get on nginx what `.htaccess` gives you on Apache.

Product images, category images and anything else you add through the media library go into `uploads/vendmizer/`, and those are public on purpose — that is how a shop shows a photo. The storefront serves them through a short /m/ URL rather than by their path on disk, but that route is not an access check of any kind: it hands the image to whoever asks for it, and the file underneath stays readable by your web server as well. So this directory gets a smaller guard than the private one — directory listing is switched off and PHP and other executable file types are refused — and that is all it needs, because the contents are meant to be seen.

Shipping labels are the third and the one to think about. When a carrier hands back a label as file data rather than as a link — which is what most of them do — the PDF is written to `uploads/vendmizer-labels/` so it can be reprinted and shared without asking the carrier again. That happens automatically, with no setting to enable, because a label that only exists inside an API response is a label you lose. A label carries the customer's name and delivery address, so treat that directory as sensitive. Three things bound it. The directory carries its own guard files — an `.htaccess`, a `web.config`, and both an `index.php` and an `index.html` — which switch directory listing off and refuse to execute anything stored there, while still allowing a plain request for a known filename, because opening a label is exactly such a request; any of them that has gone missing is written again the next time a label is cached, so a migration or a security plugin that strips them does not leave the directory bare for long. Each filename carries a sixteen-character random suffix, which is what stops one label URL leading to another. And cached labels are deleted ninety days after they are written, by the plugin's daily maintenance job, so the copy on disk has a bounded life instead of accumulating forever; that window is changeable through the `vendm_shipping_label_cache_settings` filter, including setting it to zero to keep labels indefinitely. Two honest caveats. `.htaccess` is Apache and `web.config` is IIS, so on nginx, Caddy or LiteSpeed in its non-Apache mode neither is read, the random filename is then the whole of the protection, and a link you have already sent a customer stays valid until the file is purged. And the ninety-day cleanup is registered together with the rest of the shipping features, so if you switch the Shipping module off under Settings → Modules, labels already on disk stop being purged and stay there until you delete them yourself.

One more thing goes to disk, and only if you ask for it: if you run a database backup from the plugin's own tools, the archive is written to `uploads/vendmizer-private/backups/`. That is inside the deny-all private directory described above, with the same guard files and the same nginx caveat, and the archive contains your store's database — orders, customers, addresses — so it is the most sensitive file the plugin ever writes. Backups are created only when you run one, never on a schedule, and you delete them from the same screen. Otherwise nothing goes to disk. CSV exports are built in memory and streamed straight to your browser, PDF invoices are generated when they are asked for and never stored, and the plugin writes no log files of its own. It does not keep an activity-log table either: notable events are announced on a WordPress action, `vendm_activity`, which you can attach your own listener to, and the records the plugin does keep — email sends, stock movements, webhook deliveries, shipment events, download deliveries — are ordinary rows in its own tables. Debug output goes to WordPress's own error log, and only when you have WP_DEBUG on. Uninstalling keeps your data by default; if you ask for removal instead, it drops the tables, options, roles and scheduled tasks but still leaves these directories where they are, so your media, your customers' files and any backups survive either way. If you want them gone, delete them yourself.

= Does the checkout keep an email address before the order is placed? =

Yes, and it is worth knowing. When a guest starts filling in the checkout form, the storefront saves the email address onto that shopper's own cart row a few seconds after they stop typing, and once more if they leave the page — before they have submitted anything. It is kept so that a returning shopper is recognised and so that an add-on which sends basket reminders has an address to send to. This plugin does not email it and does not list it on any admin screen; it is returned in a WordPress personal-data export, but only to whoever asks for their own data using that address. The request that carries it is nonce-protected and rate-limited, it can only write to the cart the caller already holds, and the address is deleted along with the cart, which expires thirty days after the last activity on it. No third party ever receives it.

= Can a customer get a copy of their data, or have it deleted? =

Yes. VendMizer registers with WordPress's own privacy tools, so Tools → Export Personal Data and Tools → Erase Personal Data cover the store's tables as well: orders and their shipments, the customer record, reviews, the wishlist, the live and abandoned cart, contact messages, download entitlements and their delivery log, and the notification and email history. The export explains, field by field, what was held back and why — download tokens are credentials rather than information about a person, and the download log stores a one-way hash of each IP address rather than the address itself. The plugin also contributes a section to your site's privacy policy draft, and a customer can start an erasure from a confirmation link sent to their own address.

= Does the plugin send my data anywhere? =

Not during normal use. External services are only contacted when you enable and configure the matching feature. The one exception is Gravatar, which supplies staff avatars in the admin and follows your existing WordPress avatar setting. See "External services" for the complete list and what each one sends.

= Does it have a cookie consent banner? =

Yes, and there is nothing to switch on. The moment you enter a Google Analytics or Meta (Facebook) Pixel ID, the storefront starts showing a granular consent banner and holds both scripts back until the visitor agrees. Every visitor is asked, wherever they are, and neither script loads for anyone who has not agreed — there is no geographic exemption and no way to grant a category on someone's behalf. Under Settings → Cookie Consent you can change the banner's wording, its position, its colour theme, how many months a choice is remembered, the privacy-policy link it points at, and whether a "Cookie preferences" link appears in your footer so visitors can reopen it; bumping the policy version there asks everyone again. If you set no tracking IDs, no banner appears, because the only cookies left are the strictly necessary ones that keep your basket and language working; the rest of what the store remembers on a visitor's device — a local copy of the basket, display preferences such as theme and currency, recently viewed products, search history, dismissed announcements, a returning-visitor flag, a compare list and half-filled checkout fields — is browser storage rather than cookies, is never read by anything outside your own site, and reaches your server only when the store has to act on it, as when a lost session is rebuilt from the local copy of the basket. Consent itself is stored in a single first-party cookie on the visitor's device; the plugin performs no IP lookup and keeps no server-side consent log.

= Can I put the store behind a maintenance or coming-soon page? =

Yes, under Settings → Maintenance. Pick maintenance mode or coming-soon mode, write your own title, subtitle and body in each of your languages, and optionally show a launch countdown. The two differ in what they tell a search engine, deliberately: maintenance mode answers with HTTP 503 and a Retry-After header whose length you set, so the outage reads as temporary rather than as a dead site, while coming-soon mode answers with an ordinary 200, because a launch page is real content rather than an outage. Either way you get a bypass token, and visiting your store once with `?vendm_preview=<token>` sets a twelve-hour cookie that lets you — or anyone you send the link to — browse the real store while everyone else sees the notice.

One part of it collects personal data, so it is worth stating plainly. If you switch on the optional email signup, the screen shows visitors a single email field, and an address entered there is saved to your customer list — the same list and the same table a shopper who signs up in the storefront lands in. It posts to the ordinary storefront newsletter endpoint, so it is rate-limited and validated like any other signup, it is covered by the export and erase tools described above, and nothing is sent to any third party. The field is off unless you turn it on, and there is no signup at all in maintenance mode with it off.

= Why is there a PHP file in the assets folder, and why don't the plugin's pages call wp_head()? =

Because the storefront, the admin and the customer account area are standalone documents rather than pages inside your theme. They build their own `<head>`, so `wp_head()` and `wp_footer()` are not called on them — if they were, every plugin and theme on your site would inject markup into a document it knows nothing about.

That does not mean the assets bypass WordPress. Every stylesheet and script is registered with `wp_register_style()` and `wp_register_script()` and printed by core's own `wp_print_styles()` and `wp_print_scripts()`, each called with an explicit handle, so core writes the tags and core's dependency, versioning and defer handling all apply. Nothing another plugin has enqueued is printed into these documents, because naming a handle prints only that handle.

`assets/load.php` is the concatenator. It is not reachable on its own: it carries the standard `ABSPATH` guard, it is loaded by the plugin after WordPress has booted rather than requested directly, it will only assemble bundles named in a fixed list inside the file, every path is checked with `realpath()` to stay inside the plugin directory, and the admin and account bundles additionally require a valid signature. What it returns is a concatenation of this plugin's own files and nothing else, served with a long `Cache-Control` because the URL carries a content fingerprint that changes whenever the files do.

WordPress itself works this way where it has the same problem. `wp-login.php` and `wp-admin/install.php` are core's own standalone documents: neither calls `wp_head()`, both assemble their own head, and `install.php` prints its scripts with `wp_print_scripts()` given an explicit list of handles — the same call this plugin makes for the same reason.

= Does it support languages other than English? =

Yes. English is the default language and Arabic is included out of the box (with full right-to-left layout). You can also add and edit additional languages and their translations from the admin.

= Why do some web addresses appear in VendMizer's source code that it never contacts? =

Some of what VendMizer draws is an address on someone else's domain rather than a request to it. A package-tracking number becomes a link to the carrier's own tracking page (UPS, USPS, FedEx, DHL, Aramex, SMSA, OnTrac, or AfterShip as a fallback). A product page can offer share links for WhatsApp (wa.me) and X (x.com), and every blog post carries share links for X (twitter.com), Facebook (www.facebook.com) and LinkedIn (www.linkedin.com). Fill in a WhatsApp number and a floating contact button links to wa.me too, and your footer carries whatever social profile addresses you enter. In the admin, the email settings link to Google's app-password page and the shipping settings to EasyPost's, Shippo's and USPS's developer sites (developer.usps.com); a Naqel shipment links to Naqel's own tracking page; and the map picker carries the attribution links OpenStreetMap's licence requires (www.openstreetmap.org/copyright, leafletjs.com). Every one is an ordinary link: nothing is fetched, no script is loaded, and no request reaches those hosts unless someone clicks through.

A few of the web addresses that appear in VendMizer's source are identifiers rather than places, and the plugin never fetches anything from them. The SOAP requests it builds for Aramex and Naqel Express carry the XML namespace declarations those carriers' own services require (`schemas.xmlsoap.org`, `www.w3.org`, `schemas.microsoft.com`, `tempuri.org`); the XML sitemap and the Google product feed declare their formats the same way (`www.sitemaps.org`, `base.google.com`, and `www.google.com/schemas/sitemap-image` for the image entries); the structured data on product and store pages names its vocabulary (`schema.org`); every inline SVG icon names the SVG namespace (`www.w3.org`); and the plugin header points at the licence text (`www.gnu.org`). An XML namespace is a globally unique name that is merely spelled like a URL — the format requires the exact string, and no connection is ever opened to it. The requests those envelopes are actually sent to are the Aramex and Naqel hosts listed under "Shipping carriers" above, and nowhere else.

= Which third-party libraries, icons and fonts are bundled, and under what licences? =

VendMizer bundles the open-source libraries and fonts below, and reproduces icon path data from two icon projects. Each is redistributed under its own licence, which ships beside the files it covers. `assets/vendor/README.md` records the version, origin and a SHA-256 for every bundled file — the three libraries and all eleven fonts alike — so you can fetch the same release and confirm nothing was edited on the way in. Path data is not a file and has no checksum; its licence names the source files instead.

**Chart.js 4.5.1** — MIT. Draws the charts on the admin dashboard and report screens. The plugin ships the project's official published UMD build, `assets/vendor/chartjs/chart.umd.min.js`; its complete, unobfuscated source is public at https://github.com/chartjs/Chart.js/tree/v4.5.1 . Licence: `assets/vendor/chartjs/LICENSE.md`

**Leaflet 1.9.4** — BSD 2-Clause. Powers the optional map address picker. The full unminified source ships as `assets/vendor/leaflet/leaflet-src.js`. Licence: `assets/vendor/leaflet/LICENSE.txt`

**SheetJS (js-xlsx) 0.20.3** — Apache License 2.0. Used only in the admin, to read spreadsheet files during product import; nothing is written or exported through it. The file the plugin loads is upstream's own prebuilt bundle, and both of its readable sources ship alongside it as `assets/vendor/sheetjs/xlsx.js` and `assets/vendor/sheetjs/cpexcel.js`. SheetJS left npm after 0.18.5 and publishes at https://cdn.sheetjs.com now. Licence: `assets/vendor/sheetjs/LICENSE`

Apache 2.0 is compatible with GPL version 3 but not version 2. VendMizer is "GPLv2 or later", so any distribution including SheetJS is taken under the GPLv3-or-later branch of that grant.

**Lucide icons** — ISC, and **Feather** — MIT. The interface icons are inline SVG path data from Lucide, which is a fork of Feather, and a few are Feather's original drawing; no script or stylesheet from either is bundled. Both licences, and the four files the data sits in: `assets/js/lib/LICENSE-lucide.txt`

**Brand marks.** VendMizer draws a small identifying mark beside each payment gateway in the settings, beside the Google Analytics and Meta Pixel fields, and on the WhatsApp contact button. Each is its owner's trademark, used descriptively; no owner is affiliated with VendMizer. Most are VendMizer's own artwork, and the WhatsApp handset glyph is Simple Icons' (CC0-1.0). Detail per mark: `assets/js/lib/LICENSE-brand-marks.txt`

**Fonts** — SIL Open Font License 1.1. DM Sans 4.004, Outfit 1.100 and Noto Kufi Arabic 2.109 ship as eleven .woff2 files in `assets/fonts/`, each family with its own `OFL-*.txt` licence there. No font is loaded from a third-party CDN, and every stylesheet and script VendMizer ships is served from your own site; the only externally hosted assets are the ones described under "External services".

**VendMizer's own minified JavaScript.** Every `.min.js` file under `assets/js/` is generated from the readable `.js` source that ships in the same directory — the human-readable source of every minified file is therefore included in the plugin itself. They are produced with terser by the included build script, `bin/build-min.mjs` (run `node bin/build-min.mjs` from the plugin root to regenerate them), and the asset loader falls back to the readable sources automatically when a `.min.js` file is absent, or when `&min=0` is appended to an asset URL.

= Console pages fail to load behind Cloudflare (or another CDN/firewall)? =

The admin and account consoles are single-page apps whose page modules load in the background from your own site. If a CDN or firewall in front of your site — for example Cloudflare with Bot Fight Mode, a Managed Challenge, "Under Attack" mode, or a rate rule — answers one of those background requests with a "checking your browser" challenge, the browser cannot complete that check for a background request, so the page shows "Failed to load page module." (This is most likely right after a plugin update, when every asset address is new and none is cached yet.) VendMizer now recovers automatically by reloading the console once, which lets you pass the check and the page then loads. To stop it happening at all, add a rule in your CDN that skips the bot/security challenge for requests whose query string contains `vendm_asset=1` — those are the console's own script and style bundles. On Cloudflare this is a WAF custom rule (or Configuration Rule) with the expression `http.request.uri.query contains "vendm_asset=1"` and the action Skip; it does not weaken security for the rest of your site.

== Changelog ==

= 1.65.0 =
Changes made for the WordPress.org plugin review. Each item below answers a specific point the reviewers raised, and each was tested on a clean WordPress installation with WP_DEBUG on.

* Security: every SQL statement that built part of its text from a variable was re-examined. Lists of record ids that were joined straight into an IN (...) clause are now bound one placeholder per id through $wpdb->prepare(); a handful of numbers that were concatenated into LIMIT, OFFSET and INTERVAL clauses are now bound the same way; SHOW TABLES probes are prepared; and every column or table name that reaches a statement from a variable is checked against a fixed list or an identifier pattern first, and the two filterable lists an add-on can extend are gated the same way. The branch-scope and multilingual-search fragments other queries embed are now built with $wpdb->prepare() throughout. Previously the code sniffer had been switched off over whole blocks of database code; those blanket suppressions are gone, and each remaining interpolation carries a one-line note on that line saying exactly why it is safe.
* Changed: the boot watchdog on the admin and account consoles, and the small script that prunes screen-size-gated storefront sections, are now registered and printed through WordPress's script API instead of being written as their own script elements. Every script the plugin ships now goes through that API; the only script elements it writes by hand are the two JSON data blocks (structured data and the page's boot payload), which are data rather than code.
* Documentation: the SMSA Express entry under "External services" now links to the carrier's terms and privacy policy; the admin "Test connection" and "Send test email" actions, the hosted checkout pages some gateways redirect to, and the map picker's reverse address lookup are described; and the list of web addresses that appear in the source without ever being contacted was completed.
* Internal: removed the one place where the admin-ajax path appeared as a literal string — it was in an explanatory comment, the code has always used admin_url() — so no scanner can mistake it for a hard-coded path.
* Internal: added the missing index.php to the backup directory.

= 1.64.1 =
Two fixes to how staff move between the storefront and the admin dashboard.

* Fixed: signing in through the admin portal with no return address — which includes the one-tap demo login, and any direct visit to the admin login screen — landed on the storefront home page instead of the dashboard. The step that runs after login read an empty return address as the site root; it now falls back to the dashboard, and a return address is honoured only when it points into the admin area, so a crafted link can no longer send a signing-in administrator to a storefront page.
* Added: an "Admin dashboard" button in the storefront header and menu drawer, shown only to signed-in users who can open the dashboard (a view-only account included). A manager browsing the shop as themselves now has a one-tap way back to the admin.

= 1.64.0 =
The last four items from the same review pass, each smaller than the ones in 1.63.0 and each reproduced before it was changed.

* Fixed: the product counts shown beside categories, brands and tags counted products marked "Hidden". On the storefront that meant a category could advertise more products than it would list — telling a visitor how many hidden ones it holds — and a category containing nothing but hidden products appeared in the storefront's category list and then showed an empty page. Those counts now mean "products a shopper can see". In the admin the counts are unchanged: they still include hidden products, because a merchant asking how many products are in a category means all of them.
* Privacy: a shipping method's internal settings were published in full on the public list of delivery options, which anyone can read. Nothing sensitive was being stored there — carrier credentials live elsewhere — but the field accepted anything a shop administrator typed into it. The public list now carries only the figures that describe how the shopper's own delivery is priced.
* Internal: the customer receipt and invoice links are now built from the order record that was checked, rather than from the reference in the web address, so ownership and delivery can no longer disagree about which order is meant. No behaviour change.
* Internal: corrected a comment in the shipment-tracking code that described the public tracking response as carrying no order total. It does carry one on cash-on-delivery shipments — the amount still to be collected — which the tracking page has always shown and documented, but a note that overstates a privacy property is worse than none.

= 1.63.0 =
A review pass over everything the storefront publishes without being asked to. Each item was reproduced against a running store before it was changed, and each has a test that fails without the fix.

* Security: the product feed published products the merchant had marked "Hidden". Anyone could fetch the feed — no account, no password — and read each hidden product's internal item number, title, description, price, stock availability and image, along with a link to a page the store answers with "not found". Every other public listing already left hidden products out; the feed was the one that did not, and now does.
* Security: the testimonials shown on the storefront were drawn from every approved review, including reviews of products that are hidden, still in draft, archived or deleted. Customers' words about merchandise that was never published were being shown to shoppers. Testimonials now come only from products the shop actually publishes. The same omission on the home page is fixed with it.
* Security: the sitemap handed search engines the addresses of hidden products and private blog posts — pages the store then refuses to serve — publishing their names and images to crawlers in the process. The sitemap now lists only what the store will actually show, and its totals were corrected to match so its page numbering stays right.
* Privacy: the order-tracking form told a stranger whether an order number was real. Entering a number with no email gave one answer for a real order and a different one for an invented number, so working through a run of numbers revealed which orders existed and roughly how many the shop had taken. All three ways of asking now answer the same whether or not the order exists, and tracking an order with the right email is unchanged.
* Privacy: on a store that had not filled in its own contact email, the WordPress administrator's address was published on the public Contact page and in the storefront's own configuration, which anyone can read. That address is often personal and was never offered as a shop contact. The field is now empty until the merchant enters one; the contact form works either way.
* Internal: corrected seven links to third-party terms and privacy policies in the plugin listing — three had moved and four pointed at an index rather than the document itself.

= 1.62.0 =
Security and correctness pass over the register, from an end-to-end audit. Every item below was reproduced against a running store before it was changed, and each has a test that fails without the fix.

* Security: a cashier could obtain manager approval for any manager who had not set a POS PIN, and use it to sell anything at any price with no ceiling. PINs are optional and unset by default, so on most installs every administrator was claimable. Approval now requires a PIN that exists.
* Security: the store's maximum-discount setting was enforced on one of the three ways a price can be cut. A per-line discount and a cart discount ignored it, so a shop limited to 10% could be given away in full by any cashier, with no manager and no audit entry. All three now answer to the same ceiling, and a manager's approval lifts it.
* Security: a card sale was recorded as paid without ever asking the payment gateway whether the payment existed, had succeeded, or was for the right amount. A made-up reference bought goods outright. Card payments are now verified with the gateway before the sale is recorded, and a payment short of the sale total is refused. When a gateway cannot be reached the sale still completes and is marked unverified, rather than taking the till offline.
* Security: any cashier could take over any other cashier's open register, inheriting their drawer and their held sales, including at a branch they are not assigned to. Taking over someone else's register is now a manager action and respects branch assignment.
* Fixed: the end-of-shift report expected the wrong amount of cash in two ways — it subtracted change a second time on every cash sale that gave any, and a refunded sale dropped out of the takings while the refund itself was never subtracted. Both made an accurate drawer read as over or short. The calculation now follows the money: cash taken, less change, less refunds actually handed back, plus pay-ins and minus pay-outs.
* Fixed: a sale paid entirely with a gift card could not be refunded at all — the customer was told the order had already been fully refunded. Gift card and store credit are now counted as payment for refund purposes, and a refund drawn from either credits the balance back instead of only moving a number in the ledger.
* Fixed: refunds were drawn from a gift card before cash, and nothing credited the card, so the shop handed back notes while the customer's balance stayed empty. Refunds now go back through the card that took the money first, and the methods that cannot be reversed are drawn last.
* Fixed: prices in Kuwaiti dinar, Bahraini dinar, Omani rial, Jordanian dinar, Tunisian dinar, Iraqi dinar and Libyan dinar were sent to Stripe and Square at a tenth of their value, on both charges and refunds. These currencies carry three decimal places; the conversion now knows that.
* Fixed: on a multi-location store, a register could sell stock its own branch did not hold. The check read the chain-wide total while the screen and the deduction used the branch's own. It now asks about the branch the register belongs to.
* Performance: every admin page was serving unminified JavaScript — the built files existed and were never used. Measured on a real page view, the JavaScript delivered drops from 2.33 MB to 1.31 MB.
* Performance: the transactions export ran one extra query per order. A 40,000-order export issued 40,000 of them; it now issues one per batch of a thousand.
* Privacy: per-tender payment records are now included in the personal-data export, and the free-text and staff fields on them are cleared by the erase request.

= 1.61.1 =
* Fixed: on a split payment refunded in full, the parts settled at the counter — cash and check — were not marked as refunded, so an order that had been fully refunded still showed half its payments as paid. The money was correct; the breakdown was not. Found by end-to-end testing of the refund path added in 1.61.0.

= 1.61.0 =
* Added: every payment that settles an order is now recorded individually. A sale paid with one method has one record; a split payment has one per part. Each carries its own amount, status, timestamp, and the register and cashier that took it — so a split is traceable part by part instead of existing only as a note on the order.
* Added: orders now state how much has been paid, how much is still owed, and a payment status worked out from the parts rather than asserted beside them. A part-paid order reads as part-paid.
* Added: the order screen shows the breakdown — each payment with the detail its own method calls for. A card shows its brand and last four digits, cash shows what was tendered and what came back, a check shows its number and memo.
* Added: receipts and invoices print the same breakdown, with a status marker on each part and the total paid beneath them.
* Fixed: a split payment's card legs could not be refunded through the card network. An order holds one payment reference and a split has several, so every gateway refund was skipped and the money was only ever recorded as returned. Each part is now refunded through the charge that took it, and cards are drawn down before cash so a partial refund goes back the way it came.
* Fixed: the Transactions export shipped a Card Brand and a Card Last 4 column that were always empty. Both are now filled from the order's payment records.
* Fixed: payments taken at the register could not be filtered on the Transactions page, though their amounts were counted in the totals above it. They are now listed by name — Till — Cash, Till — Card, Till — Split, Till — Other — alongside Square, Authorize.net, Klarna, gift cards and store credit, which were also missing.
* Security: the payment record stores safe metadata only — payment type, card brand, last four digits, amount, currency, status, gateway transaction id, timestamp, register and cashier. There is no field for a card number, a security code, an expiry date or a cardholder name; the last-four column is too narrow to hold a card number; and any free-text field a person types into has card-number-shaped digit runs masked before it is stored.

= 1.60.2 =
* Fixed: a printed invoice showed "[object Object]" wherever a name was stored in more than one language — the branch name on a till invoice was the visible case. Every name on a printed document now resolves to the language the document is printed in, and a name delivered in the raw storage format is decoded rather than printed as-is.
* Added: printed invoices now follow your Invoice settings, the same ones the PDF invoice uses — business name, address, phone and email, tax number under your own label, commercial registration number, document title, header and footer notes, paper size, and each of the show/hide switches for SKUs, tax, discount, shipping, payment details, billing address, order date and status.
* Added: a sale completed at the register is now marked Paid on its invoice. The register's own records carry no status field, so invoices printed from it previously showed none.
* Added: when an invoice number has been assigned, the invoice shows it as the document number and the order reference beneath it, each labelled, instead of showing only one of the two.
* Note: no database changes in this release.

= 1.60.1 =
* Fixed: after 1.60.0 the Print and Invoice buttons did nothing at all — no dialog, no message. The printing layer waited for an animation frame from the hidden print frame before opening the dialog, and a browser does not run animation frames for a frame it is not displaying, so that moment never arrived. The dialog is now opened from a timer that always runs, and the print frame is kept inside the page rather than parked outside it, so nothing about it can be skipped.
* Added: if a print never starts for any reason, VendMizer now notices and opens the document in a new tab instead, so a print can no longer fail in silence.
* Fixed: the print frame was being removed the instant the browser reported it had finished, which in some browsers is while the preview is still on screen. It is now removed a moment later.
* Note: no database changes in this release.

= 1.60.0 =
* Fixed: printing a receipt from the register produced a completely blank page. The receipt was rendered into a hidden, zero-sized frame before being sent to the printer, and a frame with no size has no page for the printer to fill — so the browser printed one empty sheet. The printing layer has been rebuilt.
* Added: a shared printing layer used by every print button in VendMizer. A printed document is now built from the order's own data rather than copied out of whatever happens to be on screen, so it no longer depends on a particular page being open, and it carries its own styling — meaning it prints the same whether you use the light or dark theme, and cannot be affected by a stylesheet failing to load.
* Added: a full-page invoice layout (A4, Letter or A5) alongside the existing till-roll receipt — with your logo and business details, the customer's details, a proper line-item table, discounts, taxes, the payment breakdown and a paid/unpaid marker. Long invoices repeat the column headings on every page and never split a line across two.
* Fixed: the logo could be missing from a printed receipt because printing started before the image had loaded. Printing now waits for images and fonts, up to a limit, so a slow logo can never hold up a sale.
* Fixed: pressing Print when there was nothing to print did nothing at all, with no message. Every print action now reports what went wrong, and offers the document in a new tab if the printer dialog cannot be opened.
* Note: no database changes in this release.

= 1.59.0 =
* Fixed: the Square API version was written out separately at every place the plugin calls Square, and the four had drifted apart — the till was asking for a different version of Square's API than the storefront and the refund path, for the same money. It is now set once, in one place, and every call uses it. You can override it from wp-config.php if Square ever ships a version you need to hold back from.
* Changed: the helper that converts an amount into the units Square expects is now available to VendMizer Pro. Pro's register carried its own copy of that conversion, and the copy was wrong for currencies that have no decimal places, such as the Japanese yen — a 5,000 yen sale at the till was sent to the card processor as 500,000 yen, while the identical sale placed online was sent correctly. Pro 1.11.73 uses this one instead of its own.
* Note: no database changes in this release.

= 1.58.0 =
* Fixed: refunding a point-of-sale sale that was paid partly by card and partly in cash asked the card processor for the whole amount. It only ever took the card part, so it refused, and the cashier was left with an error and no way forward. The card is now asked for at most what it actually took, and the reply says how much to hand back from the drawer.
* Fixed: those sales also used to be relabelled with the processor's name in the order record. That was done to make refunds work, and it broke two other things: the end-of-day cash count stopped seeing them, and receipts printed the processor's name where they meant "Card". The record keeps what happened at the counter, and the processor is stored separately.
* Note: no database changes in this release.

= 1.57.0 =
* Fixed: a helper that converts money into your base currency for reports accepted only two ways of naming the currency column and quietly substituted a third rather than saying so. In a report that reads from two tables at once, that made the database reject the whole query — and because a database error is printed as a web page rather than as data, the screen showed only "Network error." with nothing to act on. The helper now accepts any correct column name and, on an incorrect one, records it in the log instead of substituting silently.
* Internal: a new test suite covers the helper's contract and the report that broke, and the browser-level suite now fails any screen whose data request returns a web page where data was promised — the fault that hid this one.
* Note: no database changes in this release.

= 1.56.0 =
* Improved: checking a payment's status with Moyasar, Tabby or Tamara used to wait up to 30 seconds for a reply. On a busy store, a slow gateway could tie up the server that way. These checks now give up after 10 seconds and are simply retried, which is safe because asking for a status changes nothing.
* Unchanged, deliberately: captures, authorisations and refunds keep the longer 30-second wait. A timeout on one of those does not mean it did not happen — the gateway may have taken the payment and only the reply went missing — so cutting the wait short would make that outcome more likely, not less. Both limits can now be set in wp-config.php if your gateway needs different ones.
* Internal: a new browser-level test suite loads all 47 admin and storefront screens at two screen sizes and fails on a rendering fault — a stringified object, a value shown as "undefined", a screen stuck loading, a failed request or a script error. This is the class of fault that a data-only test cannot see.
* Note: no database changes in this release.

= 1.55.0 =
* Security hardening: the click-tracking rewriter that runs over outgoing email escaped each rewritten link with the general attribute escaper rather than the URL one. With click tracking switched off the link passes through untouched, so a link written into a merchant's own email template kept whatever scheme it carried. Now escaped as a URL, which drops anything that is not an allowed protocol.
* Fixed: the storefront deal-slider image, the contact-card telephone link and the WhatsApp link in the email footer were escaped the same way; all now use the URL escaper.
* Internal: the test suite now fails on any link or image in the plugin that is escaped with the wrong function for its position.
* Note: no database changes in this release.

= 1.54.0 =
* Fixed: the eight allowlists that decide an announcement's status, display type, schedule, audience, page target, dismiss behaviour, frequency and popup trigger compared loosely. On PHP 7.4 a numeric value passed the check and was stored in place of the intended default. Now compared strictly, on every supported PHP version.
* Fixed: the staff profile-change audit entry dropped the wrong field from its list whenever the record it was trying to drop was not present.
* Internal: the plugin was run through PHP_CodeSniffer with the full WordPress Coding Standards ruleset. The findings that affect behaviour are the two above; the rest are indentation and house style, and are recorded in the release notes.
* Note: no functional changes to the storefront or checkout, and no database changes, in this release.

= 1.53.0 =
* Documentation: every place the plugin prints a file, a document or a data block whole — a PDF receipt, a CSV export, the product feed, a download — carries a note in the source explaining why that output is already safe. Four of those notes were checked line by line against the code this release: two were too terse to verify and one described a different piece of code entirely. All now say what the code does.
* Internal: the full set of those notes is now covered by an automated check, so one cannot be added, removed or left unexplained without the test suite saying so.
* Note: no functional changes and no database changes in this release.

= 1.52.0 =
* Fixed: uninstalling with data removal turned on now also clears the email-verification flag it had been leaving behind on every registered customer account.
* Fixed: the settings screen said uninstalling deletes "all data". It deletes your database records; files in wp-content/uploads are deliberately left in place, which is what the documentation has always said. The on-screen wording now matches.
* Documentation: the plugin page's changelog was long enough that WordPress.org was silently cutting the oldest entries from it. Older entries now live in the bundled changelog.txt, which is where the full history has always been kept.
* Documentation: the answer about what the plugin writes to your uploads folder now mentions database backups, which are written there when you run one from the plugin's own tools.
* Note: no database changes in this release.

= 1.51.0 =

* Security: customer reviews are no longer returned for a product that is not published and publicly visible. Previously the public reviews endpoint answered for any product id, so approved reviews on a draft, pending or hidden product — including the reviewer's display name — could be read by anyone who guessed the id.
* Security: a product set to "Hidden" no longer has a working product page. The page checked that a product was published but not that it was visible, so a hidden product could still be opened by anyone who knew its web address. Catalog-only and search-only products are unaffected and still have pages, as intended.
* Security: creating, changing or deleting a package preset now requires the shipping permission rather than the order-handling permission. Presets are shared by the whole store, so a staff member who can ship orders should not be able to redefine or delete one for everybody. Reading the preset list is unchanged, so order handling is not affected.
* Security: deleting a cash-on-delivery remittance record now requires an administrator. It is an accounting record of money a courier owes the store and deleting it cannot be undone.
* Internal: sort columns and sort directions now pass through a single shared allowlist helper, so the check sits at the point of use instead of further up each function.
* Fixed: the shipping-carrier terms and privacy links for SMSA Express are described in text rather than linked, because that carrier's website serves an incomplete security certificate and the links fail validation.
* Note: no database changes in this release.

= 1.50.0 =

* Performance: the customer reviews shown on your About page load about twenty-five times faster — 92.5ms down to 3.5ms on a store with 81,000 reviews. The block asks for the best-rated approved reviews and takes six; it was reading 41,000 of them and sorting the lot to find those six. It now reads only what it shows. This is a page shoppers visit, so the time was being paid by customers rather than staff.
* Performance: the shipping classes screen is about nineteen times faster — 30.9ms down to 1.6ms. Counting the products in each class meant reading every product in the catalogue; at 100,000 products it was reading all of them to count 22.
* Performance: the transactions screen is two to three times faster — 18.3ms down to 8.9ms on the first page and 24.6ms down to 9.3ms further in. It was joining customer and user records even when nothing on screen needed them, and re-sorting the whole ledger on every page turn.
* Performance: the customer groups screen is about nine times faster — 14.9ms down to 1.6ms. Member counts are now remembered between visits and refresh immediately whenever anyone joins or leaves a group.
* Fixed: paging through the transactions screen could show the same transaction twice, or skip one, when two were recorded in the same second. The order is now settled.
* Note: this release adds two indexes on upgrade — one to the reviews table and one to the products table.

= 1.49.0 =

* Performance: the Media Library screen is around fifty times faster on a store with a large library. Opening it read every file in the library to show twenty of them — about 141ms at 50,000 files, and the same 141ms whether you were on page 1, page 50 or page 500, because the page number was never what made it slow. It now reads only the twenty it shows. Measured at 50,000 files: the library opens in 2.8ms instead of 141ms, sorting by name 2.8ms instead of 130ms, and sorting by size 2.7ms instead of 126ms.
* Performance: searching the library is around twenty times faster for the searches people actually run. Searching for the start of a file name is 2.8ms instead of 53ms, and a search matching most of the library is 2.9ms instead of 73ms. Searching for text from the middle of a file name is 15.7ms instead of 41.5ms; a search matching nothing at all is slightly slower than before (57ms against 52ms), because it now tries the fast path first and then the thorough one.
* Performance: the library's storage summary and folder counts are now remembered between visits instead of recounted every time — 2.1ms instead of 59.6ms for the summary, and the folder list no longer costs 631ms the first time it is opened after a quiet period. They refresh immediately whenever a file or folder changes.
* Fixed: paging through the Media Library could show the same file twice, or skip one, when several files shared an upload time. 50,000 files in testing shared only 1,803 distinct timestamps, with up to 53 files on one, so the order within a group was arbitrary and could differ between pages. The order is now settled.
* Note: this release adds four indexes to the media table on upgrade.

= 1.48.0 =

* Fixed: on some screens a loading placeholder could briefly show the text "[object DocumentFragment]" instead of the grey loading bars, until the real content arrived a moment later. The placeholder helper accepted a number of rows in a way that produced that text rather than the bars; it now produces the bars, as it was always meant to.
* Note: no database changes in this release.

== Upgrade Notice ==

= 1.64.0 =
Category, brand and tag product counts no longer include hidden products on the storefront, and shipping methods no longer publish their internal settings on the public delivery-options list. No database changes.

= 1.63.0 =
Privacy release. The product feed, testimonials and sitemap published products and posts the shop had chosen to hide, order tracking revealed whether an order number was real, and an unconfigured store published the site admin's email address. No database changes.

= 1.62.0 =
Security and money release for the register: closes a price-override bypass and an unverified-card-payment hole, corrects the end-of-shift cash count, makes gift-card sales refundable, and fixes three-decimal currencies charged at a tenth. Recommended for every store using the register.

= 1.61.1 =
Corrects the payment breakdown after a full refund of a split payment: the cash and check parts are now marked refunded along with the card parts. No database changes.

= 1.61.0 =
Adds a table so each payment on an order is recorded separately. Split payments become traceable and their card parts refundable through the card network; orders show what has been paid and what is owed. Safe card metadata only — no card numbers are stored.

= 1.60.2 =
Printed invoices no longer show "[object Object]" for names stored in more than one language, and now follow your Invoice settings. No database changes.

= 1.60.1 =
Required if you installed 1.60.0. The Print and Invoice buttons did nothing in a real browser. No database changes.








