20 Commits

Author SHA1 Message Date
coraislovely-code df577938aa Keep legacy-named translation files loading
No language pack ever loaded under the old 'comiceasel' domain, but a site
that compiled its own comiceasel-{locale}.mo did load it, and the rename
would have dropped that translation without a word. A load_textdomain_mofile
filter answers with the legacy filename when nothing exists under the new
one, looking both in wp-content/languages/plugins and in the directory
WordPress asked about, since 6.7 names the registered domain directory rather
than the one the file is in.

The explicit load_plugin_textdomain() call is kept and commented for the same
reason: on WordPress before 6.7 nothing else requests a translation file by
name, so it is the only place the filter can intervene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:11:05 -04:00
coraislovely-code df7a6f78ba Align the text domain with the plugin slug
The domain has been 'comiceasel' since 2012 while the wordpress.org slug is
'comic-easel'. Language packs from translate.wordpress.org are keyed to the
slug, so they could never load; no working translation ever shipped against
the old domain. Renames the domain in every gettext call, in
load_plugin_textdomain(), in both Text Domain headers, and on the lang/
template files, which already carried comic-easel domain markers internally.

Option keys, admin page slugs, nonce actions and style handles that share the
comiceasel prefix are deliberately left alone; only the $domain argument moved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:48:00 -04:00
coraislovely-code 1d0cabb1b0 Escape widget output and field attributes
Widget form() methods printed get_field_id()/get_field_name() straight into
id and name attributes, and the labels used _e(). Wrap the field helpers in
esc_attr(), switch the labels to esc_html_e(), and swap strip_tags() for
wp_strip_all_tags() on the stored titles. Adds the matching test stubs.

Assembled markup ($before_widget, filtered titles, $output) is left alone;
escaping it would change what existing sites render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:33:55 -04:00
coraislovely-code 95a4638d8f Load cast hover assets relative to the plugin folder
The cast hovercard widget built its CSS and JS URLs from a literal
comic-easel/ path, so the widget lost its assets whenever the plugin
was installed under any other directory name. Use the plugin URL the
rest of the plugin already resolves at runtime, and version the style
to match the script it pairs with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:22:32 -04:00
coraislovely-code 8f52144eff Harden database queries and request input
Co-authored-by: gpt-5.6-sol <noreply@openai.com>
2026-08-03 10:57:47 -04:00
coraislovely-code e65602cba5 Load PayPal helpers in output escaping tests
Co-authored-by: gpt-5.6-sol <noreply@openai.com>
2026-07-30 08:25:02 -04:00
coraislovely-code 356a5fb675 Merge pull request #45 from coraislovely-code/codex/harden-output-escaping
Escape remaining public output contexts
2026-07-30 08:20:27 -04:00
coraislovely-code 2647482d1d Escape remaining public output contexts
Co-authored-by: gpt-5.6-sol <noreply@openai.com>
2026-07-29 09:13:08 -04:00
coraislovely-code ba4625967e Harden PayPal IPN validation
Co-authored-by: gpt-5.6-sol <noreply@openai.com>
2026-07-29 09:09:42 -04:00
coraislovely-code 9bfaf55628 Remove defunct Blind Ferret integration 2026-07-28 18:26:55 -04:00
coraislovely-code 86cd617ee7 Document how to exercise the PayPal endpoint locally
The manual-testing notes said the endpoint could not be reached without a
PayPal sandbox account, which was true when they were written. This branch
adds the filters that make a local stand-in possible, so document them.

The recipe and the expectation table are what was actually used to verify this
branch against a real WordPress install: a mu-plugin answering the handshake,
a filter capturing the notification email, and a request per case.

The reject paths are listed alongside the accept path deliberately. Each is
meant to state its reason in the email to the owner rather than fail silently,
and that is only true if someone checks it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:21:44 -04:00
coraislovely-code d867fdb60c Test the comic HTML trust decision and the meta box round trip
Covers the trust matrix for the comic HTML fields -- author capability, last
editor capability, missing post -- and asserts that decoding happens before
the trust check.

Also pins the meta box round trip: a value stored by the meta box renders
identically to before, while a value stored raw is inert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:21:44 -04:00
coraislovely-code 43122e9a97 Fall back to the default price the same way the buy form does
The price lookup treated a stored zero as a real price while the form and the
meta box both treated it as unset, so a comic with a zero recorded price
showed the default on the form but expected zero from the notification --
which disabled the amount check for that cart.

Use the same emptiness test as the other two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:21:44 -04:00
coraislovely-code ece7cdafce Recognise the original label in any locale
The label is translated when the notification arrives, but the item name was
built when the form was rendered. If the site language changed between those
two moments, originals were read as prints.

Match the translated label or the untranslated one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:21:44 -04:00
coraislovely-code 8c9ab4b8a8 Stop a posted cmd overwriting the IPN validation command
The validation command was merged first but the notification merged over it,
so a submitted cmd field replaced it. This fails closed today, since PayPal
will not confirm a request that is not a validation request, but it is one
refactor away from mattering.

Build the body so the validation command is both first and authoritative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:21:44 -04:00
coraislovely-code 87c3d09bb8 Stop one bad byte blanking a transcript or refer-only message
Passing ENT_QUOTES alone to htmlspecialchars() replaces PHP 8.1's default flag
set rather than adding to it, dropping ENT_SUBSTITUTE -- so a single invalid
UTF-8 byte made the call return an empty string and the whole field rendered
blank.

Pass ENT_SUBSTITUTE explicitly, and fall back to UTF-8 when blog_charset is
empty or unrecognised, which produces the same empty result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:21:44 -04:00
coraislovely-code 836c4f6343 Test that shortcode attributes cannot reach the SQL
Asserts the emitted SQL for the archive and character-list queries: the
chapter is always an integer and the sort direction is always the literal ASC
or DESC, whatever the shortcode attribute contained.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 13:38:27 -04:00
coraislovely-code 64cdd8e261 Document how to test the parts unit tests cannot reach
The unit suite covers the plugin's logic. It cannot cover the settings forms, the comic meta
boxes, the widget forms or the admin list columns, because those are templates with no
function boundary to call -- and they are exactly where a broken escaping change shows up as
something a user notices.

Testing those needs a real WordPress, and getting this plugin into a state where its
interesting code paths actually run turns out to be full of traps that make working features
look broken: the plugin directory has to be named comic-easel or one widget loses its
assets; it has to be activated through the Plugins screen because activation adds a column
that several queries depend on; chapter Order defaults to 0 and 0 makes one archive mode
render empty; a comic with no featured image renders as nothing but an HTML comment; any
value in refer-only hides the comic from everybody; enabling transcripts in posts makes the
transcript shortcode return nothing by design.

None of that was written down anywhere. It is now, along with how to reach the PayPal IPN
endpoint locally -- which otherwise cannot be exercised at all, since it exits unless PayPal
returns VERIFIED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 13:15:07 -04:00
coraislovely-code 2fe70a3d87 Add characterization tests for the code about to be changed
Security fixes to output escaping carry a specific risk: the fix silently changes what
users see. These tests pin the behaviour that must NOT change, so that any such drift shows
up as a failing assertion rather than as a bug report.

Chosen deliberately to be stable across the fixes that follow, so they stay green
throughout rather than needing to be rewritten by each one. The security assertions -- that
a shortcode attribute cannot reach the SQL string, that a javascript: URI cannot be stored
as a URL -- ship with the commits that make them true, so each fix arrives with the test
that would have caught its absence.

The two most valuable assertions here encode properties rather than values:

- Rendering a transcript must be stable across saves: a value stored the way the save
  handler stores it, through esc_textarea(), must render back as exactly those bytes. The
  case that discriminates is an author who typed a literal entity -- esc_textarea() stores
  "&lt;b&gt;" as "&amp;lt;b&amp;gt;", and an escape that declines to re-encode existing
  entities hands back one level short, so the author's literal text decays into a live tag a
  save at a time.
- The same round-trip property for widget link labels, fed through update() twice.

ArchiveQueryTest uses the $wpdb spy rather than a database, and asserts that table names
come from $wpdb rather than a hardcoded prefix -- a query with the prefix baked in silently
returns nothing on any site that did not accept the installer default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 08:36:46 -04:00
coraislovely-code 1a405071aa Add a PHPUnit harness that runs without WordPress
The plugin has never had a test of any kind. The obstacle has always been that testing a
WordPress plugin appears to require WordPress, and therefore a database.

It does not, for the part that matters. The plugin's logic -- escaping decisions, SQL
construction, payment validation -- depends only on its arguments plus a small set of
WordPress helpers. Stubbing those gives a suite that runs in milliseconds with no database,
no Docker and no WordPress checkout.

Three of the stubs have to be faithful or the tests they support quietly stop meaning
anything, and this is documented at the top of tests/stubs.php:

- esc_html()/esc_attr() call _wp_specialchars() with $double_encode = false, so they leave
  existing entities alone, while esc_textarea() double-encodes. Much of this plugin's
  escaping behaviour turns on that difference, so a naive htmlspecialchars() stub would
  give the wrong answer.
- wp_kses_post() is a recorder returning a sentinel rather than a reimplementation. The
  question worth asking is "was filtering applied", not "what did it strip".
- apply_filters() passes through by default but is overridable, because the plugin's
  filters are the seams the tests need.

Also included is a $wpdb spy that records the SQL it is handed. That is what allows the
query construction to be tested without a database at all -- which matters here, because
the plugin's queries use MySQL-only date functions that a SQLite test database would
misreport.

HarnessTest.php asserts these properties of the harness itself. If it fails, nothing else
in the suite should be trusted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 08:36:22 -04:00