=== DSS Artwork Upload ===
Contributors: dss
Tags: woocommerce, upload, artwork, file upload, product add-ons
Requires at least: 6.0
Tested up to: 6.8
Requires PHP: 7.4
WC requires at least: 7.0
WC tested up to: 9.9
Stable tag: 1.0.2
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Turns the plain "Upload your artwork" file input on WooCommerce product pages
into a branded, accessible drag-and-drop uploader — and hard-blocks every file
type outside JPG / PNG / GIF / WebP / PDF on the server.

== Description ==

**What it does**

* Finds the `<input type="file">` inside `form.cart` on single-product pages and
  wraps it in a drag-and-drop zone styled with the DSS brand yellow (`#FFD623`,
  sampled from the logo and the "Add to cart" button).
* Shows a live thumbnail for JPG / JPEG / JPE / PNG / GIF / WebP.
* Shows a PDF document icon for PDF.
* Rejects everything else — on the client for speed, and again on the server,
  where it actually matters.
* Full keyboard and screen-reader support; `prefers-reduced-motion` and
  `forced-colors` are respected.

**What it deliberately does NOT do**

It does not render the field, name the field, store the file, or attach it to the
order. Whichever product-add-on plugin currently owns "Upload your artwork" keeps
doing all of that. This plugin is a *progressive enhancement* layer plus a
*validation* layer. The original input is moved — never replaced, cloned or
renamed — so the owning plugin's `name` attribute, its own JavaScript, and its
server-side handler all keep working untouched.

**Security model**

The `accept` attribute and the JavaScript checks are convenience. They can be
stripped with dev-tools in four seconds. The real gate is
`includes/class-security.php`, which runs on `woocommerce_add_to_cart_validation`
— the hook that fires *before* `woocommerce_add_cart_item_data`, where add-on
plugins move the upload onto disk. A file must pass all of the following:

1. PHP reported no upload error and `is_uploaded_file()` agrees.
2. Size is greater than zero and within the effective ceiling.
3. The raw filename contains no null byte, path separator, `..`, or executable
   extension at any position (blocks `logo.php.png`).
4. The sanitised extension is on the allow-list.
5. `wp_check_filetype_and_ext()` agrees on both extension and MIME, and offers no
   `proper_filename` correction.
6. `finfo` — when the extension is compiled in — reports exactly the MIME the
   extension implies.
7. The file's magic bytes match the extension.
8. Raster images additionally survive `getimagesize()`. WebP is parsed by hand,
   because `getimagesize()` WebP support varies across PHP builds.
9. The first 8 KiB contain no `<?php` opening tag.

`wp_handle_upload_prefilter` is wired as a second layer for add-on plugins that
call `wp_handle_upload()` themselves. It only enforces during an add-to-cart
request, so the media library and other front-end uploaders are untouched.

SVG is **never** allowed. It is a scriptable document, not an image.

== Installation ==

1. Upload the `dss-artwork-upload` folder to `/wp-content/plugins/`.
2. Activate it from **Plugins**.
3. There are no settings. Everything is a filter — see below.

For a 100 MB limit to be real, the server must also allow it. Check
**Tools → Site Health → Info → Media**, or `phpinfo()`, for:

    upload_max_filesize = 100M
    post_max_size       = 105M
    max_input_time      = 300
    memory_limit        = 256M

If `upload_max_filesize` is lower, the plugin automatically advertises the lower
number rather than promising something PHP will silently discard.

== Filters ==

`dss_artwork_upload_types`
: The allow-list, keyed by lower-case extension. Each entry needs `mime`,
`preview` (`image` or `pdf`) and a `magic` token the sniffer understands
(`jpeg`, `png`, `gif`, `webp`, `pdf`). Adding an extension without a matching
magic token causes that extension to be rejected — by design, it fails closed.

`dss_artwork_upload_max_bytes( int $effective, int $server_ceiling )`
: The effective maximum in bytes. Raising it above PHP's own limits has no
effect; PHP rejects the request before WordPress boots.

`dss_artwork_upload_preview_max_bytes( int $bytes )`
: Images above this size get a glyph instead of a real thumbnail, because
decoding a 90 MB PNG on the main thread locks the browser tab. Default 25 MB.

`dss_artwork_upload_selector( string $selector )`
: CSS selector for the file input(s). Default `form.cart input[type="file"]`.
Narrow this if a second, unrelated file input ever lands inside the cart form.

`dss_artwork_upload_should_enqueue( bool $should )`
: Load the assets somewhere other than `is_product()` — a quick-view modal, say.

`dss_artwork_upload_field_names( string[] $names )`
: Restrict server-side validation to specific `$_FILES` keys. Default: every key
on an add-to-cart request.

`dss_artwork_upload_enforce( bool $enforce )`
: Whether `wp_handle_upload_prefilter` enforces the artwork rules on this
request.

`dss_artwork_upload_blocked_name_pattern( string $pattern )`
: Pipe-separated regex alternation of extensions never permitted anywhere in a
filename, matched at any position — so `logo.php.png` is rejected even though
its final extension is allowed.

`dss_artwork_upload_runtime_config( array $config )`
: The JSON object handed to the browser.

== Manual test checklist ==

Client:

* Select a PNG / JPG / GIF / WebP → thumbnail appears, name + size + badge shown.
* Select a PDF → PDF glyph appears.
* Select a `.txt`, `.svg`, `.zip`, `.exe` → red error, input cleared, nothing submitted.
* Rename `payload.php` to `payload.php.png` and select it → server rejects it.
* Drag a file over the zone → border turns solid yellow. Drop → file attaches.
* Drag two files onto a single-file input → "one file at a time" error.
* Tab to the zone → visible focus ring. Enter/Space opens the picker.
* Select, then Remove → returns to the empty state, focus returns to the field.
* Select, then Replace → picker reopens, old object URL is revoked.
* Switch a product variation → the field re-renders and stays enhanced.
* Add to cart with a valid file → cart item carries the artwork as before.

Server (the part that actually matters):

* `curl -F` an `.svg` straight at the add-to-cart endpoint → rejected.
* `curl -F` a `.png` whose bytes are really a PHP script → rejected.
* `curl -F` a real PNG renamed to `.pdf` → rejected (MIME/extension mismatch).
* Upload something larger than `post_max_size` → a clear notice, not a silent reload.

== Changelog ==

= 1.0.2 =
* Fix: 1.0.1's `all: revert` reset used a universal `.dss-au *` selector,
  which also matched the inline SVG icons' `path` / `circle` / `rect` /
  `text` elements. Those icons are coloured with `fill="currentColor"` /
  `stroke="currentColor"` PRESENTATION ATTRIBUTES rather than CSS — and
  presentation attributes sit at the bottom of the author-origin cascade,
  so `all: revert` stripped them, reverting to the SVG user-agent default
  (`fill: black`, `stroke: none`). Depending on the icon, this made it
  disappear entirely or render as a solid black blob instead of the
  intended outline glyph.
* The reset now names the exact tags this component creates (label, span,
  ul, li, p, button, img) instead of a universal selector, so it can never
  touch svg/path/circle/rect/text. The rest of 1.0.1's fix — the
  `.dss-au .dss-au__x` specificity rescoping and the targeted `!important`
  flags — is unchanged and still verified against the same cascade tests.

= 1.0.1 =
* Fix: the drop zone, preview cards, and error box could lose their flex
  layout to theme/WooCommerce CSS (e.g. `.form-row label { display: block; }`
  or similar class+element rules), because every plugin selector was a
  single class — the lowest possible specificity. Symptoms looked like the
  headline, "or", button, and hint text running together on one line with
  no spacing, the hint wrapping mid-word, and the upload icon sitting above
  the box instead of centred inside it.
* Every component selector is now scoped two levels deep
  (`.dss-au .dss-au__x`) and paired with an `all: revert` reset on the whole
  component, so it structurally outranks the common theme patterns that
  caused this. Layout-critical `display` / `flex-direction` / `align-items`
  declarations also carry `!important` as a second line of defence.
* No HTML, JS, or PHP changes — this is a CSS-only fix.

= 1.0.0 =
* Initial release.
