Ownware
Home›Revup›Test run
Revup · Test run · run 2026-09-25

Revup Test run, as shipped in the download

336 checks passed, 0 failed, when the suite that ships in the zip was run on 2026-09-25 with php tests/run.php. Run it yourself after you download.

== 1. Template rendering ==
  PASS  all three placeholders (got 'Hi Ava, thanks from Purple Dental: https://x/r/t1', want 'Hi Ava, thanks from Purple Dental: https://x/r/t1')
  PASS  repeated placeholder (got 'Bo Bo', want 'Bo Bo')
  PASS  missing var -> empty (got 'Hi !', want 'Hi !')
  PASS  unknown braces untouched (got 'A {other}', want 'A {other}')
  PASS  no placeholders -> passthrough (got 'plain text', want 'plain text')
  PASS  HTML in value stays raw in message body (got '<b>x</b>', want '<b>x</b>')
  PASS  e() escapes for view context (got '&lt;b&gt;&quot;x&quot;&lt;/b&gt;', want '&lt;b&gt;&quot;x&quot;&lt;/b&gt;')
  PASS  firstName picks first word (got 'Ava', want 'Ava')
  PASS  firstName empty -> "there" (got 'there', want 'there')

== 2. Tokens ==
  PASS  token is 40 hex chars
  PASS  200 generated tokens are unique (got 200, want 200)

== 3. Open flow: every rating is shown the review link (threshold boundaries) ==
  PASS  open flow: the old gate decision (isPublic) no longer exists
  PASS  default 4: rating 3 -> also offered a private message (got true, want true)
  PASS  default 4: rating 4 -> no private offer (got false, want false)
  PASS  default 4: rating 1 -> also offered a private message (got true, want true)
  PASS  threshold 5: rating 4 -> also offered (got true, want true)
  PASS  threshold 3: rating 3 -> no private offer (got false, want false)
  PASS  threshold 1: private offer is off for every rating (got false, want false)
  PASS  threshold clamped: 0 behaves as 1 (off) (got false, want false)
  PASS  threshold clamped: 9 behaves as 5 (got true, want true)
  PASS  an invalid rating is never offered anything (got false, want false)
  PASS  state: unrated -> the stars (got 'rate', want 'rate')
  PASS  state: rated at/above threshold -> review links (got 'review', want 'review')
  PASS  state: rated below threshold, no message -> review links + private form (got 'feedback', want 'feedback')
  PASS  state: private message sent -> thanks + review links (got 'thanks', want 'thanks')
  PASS  open flow: no rating under any threshold lands in a state without the review link
  PASS  reviewLinks: primary offered in slot 1 with its label
  PASS  reviewLinks: a non-http(s) URL is never offered
  PASS  parseRating "5" -> 5 (got 5, want 5)
  PASS  parseRating "4" -> 4 (got 4, want 4)
  PASS  parseRating 0 -> null (got NULL, want NULL)
  PASS  parseRating 6 -> null (got NULL, want NULL)
  PASS  parseRating "abc" -> null (got NULL, want NULL)
  PASS  parseRating null -> null (got NULL, want NULL)

== 4. Clip/clamp on writes (MySQL strict-mode defense) ==
  PASS  contact name clipped to 190 (got 190, want 190)
  PASS  contact email clipped to 190 (got 190, want 190)
  PASS  contact phone clipped to 40 (got 40, want 40)
  PASS  contact tags clipped to 255 (got 255, want 255)
  PASS  campaign name clipped to 190 (got 190, want 190)
  PASS  campaign subject clipped to 190 (got 190, want 190)
  PASS  campaign template clipped to 10000 (got 10000, want 10000)
  PASS  bogus channel coerced to email (got 'email', want 'email')
  PASS  updateContact lowercases email (got 'ava@x.com', want 'ava@x.com')
  PASS  clamp_int caps range (got 2000000000, want 2000000000)
  PASS  clamp_int floor (got -2000000000, want -2000000000)
  PASS  clamp_int non-numeric -> 0 (got 0, want 0)

== 5. Request creation + single-use rating ==
  PASS  creates one request per valid unique contact (got 2, want 2)
  PASS  request tokens unique + hex
  PASS  new requests are pending (got 'pending', want 'pending')
  PASS  last_request_at stamped on contact
  PASS  re-create while open request exists -> skipped (got 0, want 0)
  PASS  canRate true before rating
  PASS  recordRating consumes token (got true, want true)
  PASS  recordRating second time rejected (single-use) (got false, want false)
  PASS  first rating persisted, not overwritten (got 5, want 5)
  PASS  rated_at stamped
  PASS  canRate false after rating
  PASS  rating alone does not mark the review page opened (got 0, want 0)
  PASS  markReviewOpened: an unrated request cannot be marked (got false, want false)
  PASS  markReviewOpened: first open is recorded (got true, want true)
  PASS  markReviewOpened: a second open is not a new event (webhook fires once) (got false, want false)
  PASS  markReviewOpened: redirected stamped (got 1, want 1)

== 6. Feedback storage + clipping ==
  PASS  feedback message clipped to 5000 (never 22001) (got 5000, want 5000)
  PASS  feedback rating stored (got 2, want 2)
  PASS  feedback starts unhandled (got 0, want 0)
  PASS  feedback rating clamped to 5 (got 5, want 5)

== 7. CSV import mapping ==
  PASS  mapped rows (got 3, want 3)
  PASS  blank row skipped (got 1, want 1)
  PASS  alias headers matched (Full Name) (got 'Ava T', want 'Ava T')
  PASS  email lowercased + kept (got 'ava@x.com', want 'ava@x.com')
  PASS  invalid email dropped to empty (got '', want '')
  PASS  oversize CSV name clipped to 190 (got 190, want 190)
  PASS  oversize CSV phone clipped to 40 (got 40, want 40)
  PASS  oversize CSV tags clipped to 255 (got 255, want 255)
  PASS  UTF-8 BOM on first header stripped (got 'Zed', want 'Zed')
  PASS  unmappable header -> nothing imported (got 0, want 0)
  PASS  row cap enforced (got 10, want 10)
  PASS  rows beyond cap counted as skipped (got 20, want 20)
  PASS  nameless row falls back to email (got 'only@x.com', want 'only@x.com')

== 8. CSV import into DB (dedupe by email) ==
  PASS  inserted (got 2, want 2)
  PASS  updated (deduped by email) (got 1, want 1)
  PASS  no duplicate rows created (got 1, want 1)
  PASS  update keeps old phone when new blank (got '+1777', want '+1777')
  PASS  formula content stored RAW in DB (guard is at export) (got '=cmd()', want '=cmd()')

== 9. CSV formula-injection guard (numeric-aware) ==
  PASS  =-formula neutralized (got '\'=1+2', want '\'=1+2')
  PASS  @-formula neutralized (got '\'@SUM(A1)', want '\'@SUM(A1)')
  PASS  +cmd neutralized (got '\'+cmd', want '\'+cmd')
  PASS  +1 numeric -> KEPT (got '+1', want '+1')
  PASS  -text neutralized (got '\'-abc', want '\'-abc')
  PASS  negative number KEPT (got '-45.00', want '-45.00')
  PASS  leading TAB neutralized (got '\'	X', want '\'	X')
  PASS  leading CR neutralized + quoted
  PASS  plain value untouched (got 'Ava T', want 'Ava T')
  PASS  comma value quoted (got '"a,b"', want '"a,b"')
  PASS  quote value escaped (got '"say ""hi"""', want '"say ""hi"""')
  PASS  export header row (got 'name,phone', want 'name,phone')
  PASS  CRLF endings
  PASS  formula neutralized in real export
  PASS  negative number preserved in export
  PASS  empty export -> empty string (got '', want '')

== 10. Mailer message building (no socket) ==
  PASS  From header present
  PASS  To header present
  PASS  Subject header present
  PASS  MIME text/plain utf-8
  PASS  CRLF header/body separator
  PASS  dot-stuffing applied
  PASS  CRLF stripped from To (header injection)
  PASS  CRLF stripped from Subject (header injection)
  PASS  encodeHeader ASCII passthrough (got 'Hello', want 'Hello')
  PASS  encodeHeader UTF-8 -> RFC 2047 (got '=?UTF-8?B?Q2Fmw6k=?=', want '=?UTF-8?B?Q2Fmw6k=?=')
  PASS  dotStuff normalizes lone LF to CRLF (got 'a
b', want 'a
b')
  PASS  dotStuff escapes leading dot at start (got '..x', want '..x')
  PASS  send refuses when unconfigured (no socket)
  PASS  send refuses invalid recipient before any socket
  PASS  Mailer::configured false on blank
  PASS  Mailer::configured true on host+from

== 11. Sms request building (no network) ==
  PASS  URL hard-coded to api.twilio.com
  PASS  SID rawurlencoded in path (no traversal)
  PASS  URL ends with /Messages.json
  PASS  From urlencoded
  PASS  To urlencoded
  PASS  Body urlencoded (& escaped)
  PASS  auth is sid:token (got 'AC123/../evil:tok', want 'AC123/../evil:tok')
  PASS  SMS body clipped to 1600 (got 1600, want 1600)
  PASS  Sms::configured false on blanks
  PASS  Sms::send refuses when unconfigured (no network)
  PASS  Sms::send refuses invalid phone before network

== 12. Sender modes: demo-block, manual, missing address ==
  PASS  demo mode blocks send, marks sent (got 'sent', want 'sent')
  PASS  demo mode via = demo (got 'demo', want 'demo')
  PASS  demo outcome persisted on row (got 'demo', want 'demo')
  PASS  demo sent_at stamped
  PASS  manual mode keeps pending (got 'pending', want 'pending')
  PASS  manual mode via = manual (got 'manual', want 'manual')
  PASS  manual persisted as pending (got 'pending', want 'pending')
  PASS  missing address -> failed (got 'failed', want 'failed')
  PASS  missing address error message set
  PASS  sms unconfigured -> pending/manual (got 'pending/manual', want 'pending/manual')
  PASS  gateLink contains /r/<token>

== 13. Gate rate limit (per-IP) ==
  PASS  61st hit from same IP blocked
  PASS  different IP unaffected

== 14. Auth ==
  PASS  password hash verifies
  PASS  stored hash verifies
  PASS  wrong password rejected

== 15. Stats ==
  PASS  stats sent >= 1
  PASS  stats rated counts ratings
  PASS  stats avg is float or null
  PASS  below + at_or_above == rated
  PASS  stats: opened_review counts raters who opened the review page
  PASS  stats: the gate-era keys are gone (no "kept private" figure)

== 16. Security hardening regressions ==
  PASS  csrf_check rejects empty stored token (hash_equals empty-token bypass)
  PASS  installer rejects empty install token (_it bypass)
  PASS  settings_save blocks writes in public demo mode
  PASS  Sender demo-block sits BEFORE any credential/config read
  PASS  Twilio endpoint hard-coded https://api.twilio.com
  PASS  Twilio client never follows redirects
  PASS  no error_log of credentials anywhere in senders
  PASS  settings view never echoes smtp_pass value
  PASS  settings view never echoes twilio_token value
  PASS  settings view never echoes twilio_sid value
  PASS  demo session cookie is None + Secure + Partitioned
  PASS  buyer session cookie is HttpOnly + Lax + Secure-on-HTTPS
  PASS  gate view shows first name only (no email/phone echoed)
  PASS  gate feedback form carries the honeypot field
  PASS  gate handlers enforce the per-IP rate limit
  PASS  gate token lookup is parameterized
  PASS  public review links carry rel=noopener
  PASS  dev router denies config.php / data / .sqlite
  PASS  .htaccess denies config.php / sqlite / demo marker
  PASS  portable UTC timestamps (no SQLite datetime() in controllers)
  PASS  no stack traces: front controller catches boot errors

== 3.1.4 open flow (rendered rating page) ==
  PASS  open flow: the 'review' page links to the public review page
  PASS  open flow: the 'feedback' page links to the public review page
  PASS  open flow: the 'thanks' page links to the public review page
  PASS  open flow: a low rating sees the review link BEFORE the private form, never instead of it
  PASS  open flow: the private form is only on the below-threshold page
  PASS  open flow: the page never tells a customer their message replaces a review
  PASS  open flow: gate_rate never sends a rating straight to an external review URL
  PASS  open flow: the review click-through route is registered and records the open
  PASS  open flow: settings can switch the private offer off (threshold 1 is selectable)
  PASS  open flow: the dashboard no longer sells a count that "never reached a public profile"
  PASS  tile trend: six months, oldest first (got array (
  0 => 'Apr',
  1 => 'May',
  2 => 'Jun',
  3 => 'Jul',
  4 => 'Aug',
  5 => 'Sep',
), want array (
  0 => 'Apr',
  1 => 'May',
  2 => 'Jun',
  3 => 'Jul',
  4 => 'Aug',
  5 => 'Sep',
))
  PASS  tile trend: counts SENT requests by send month (pending/failed and creation month ignored) (got array (
  0 => 0,
  1 => 0,
  2 => 0,
  3 => 0,
  4 => 1,
  5 => 1,
), want array (
  0 => 0,
  1 => 0,
  2 => 0,
  3 => 0,
  4 => 1,
  5 => 1,
))
  PASS  tile trend: the quarter total can never exceed the "Requests sent" total
  PASS  tile trend: the dashboard reads Revup::sentPerMonth, not created_at

== PWA ==
  PASS  manifest.json is valid JSON
  PASS  manifest has required fields
  PASS  manifest URLs are RELATIVE (subpath-install safe)
  PASS  manifest icons exist on disk (192 + 512 + apple-touch)
  PASS  sw.js never caches non-GET
  PASS  sw.js never caches page navigations (auth HTML can not enter cache storage)
  PASS  sw.js cache name carries the version (old caches purged on activate)
  PASS  offline fallback page ships
  PASS  every app layout links the manifest + registers the SW

-- Own It 2.0 --
  PASS  v2: tables
  PASS  v2: totp_cols
  PASS  v2: notify_cols
  PASS  v2: smtp_preexisting
  PASS  v2: mint
  PASS  v2: revoke
  PASS  v2: totp
  PASS  v2: recovery
  PASS  v2: delivery
  PASS  v2: rate
  PASS  v2: csv_dry
  PASS  v2: csv_refuses_unreachable
  PASS  v2: backup_tables
  PASS  v2: gate_reuse
  PASS  v2: feedback_no_token
  PASS  v2: request_sent_fires_from_the_sender
  PASS  v2: request_sent_not_refired_by_a_followup
  PASS  v2: runner_releases_what_is_due
  PASS  v2: runner_second_pass_same_day_is_a_noop
  PASS  v2: runner_followup_waits_for_the_gap
  PASS  v2: runner_followup_skips_who_answered
  PASS  v2: runner_followup_leaves_the_first_send_alone
  PASS  v2: runner_followup_is_sent_once
  PASS  v2: runner_stamps_every_pass
  PASS  v2: runner_respects_the_off_switch
  PASS  v2: routes registered (api, 2fa, security, backup, import, healthz)
  PASS  v2: 2FA intercepts login + login is rate-limited
  PASS  v2: negative feedback notifies the operator + fires a webhook, never fatally
  PASS  v2: reuses revup OWN Mailer — MailerLite is deliberately NOT shipped
  PASS  v2: API reuses the UI core (Revup::createRequests + _send_requests)
  PASS  C2: requireKey filters users.active (deactivating a user revokes their key)
  PASS  v2: no ternary key re-reads in ANY controller (PHP 8 warning hygiene, PLAN item 10)
  PASS  v2: dark palette + zero hardcoded white backgrounds
  PASS  v2: theme.js + qr.js ship and layout loads theme.js + a toggle
  PASS  v3: API.md ships + sw cache is v3.1.5 (and not the old name, or a changed stylesheet serves stale)
  PASS  v2: cli-server base guard present
  PASS  v3: index.php calls Database::migrate() at boot (1.x/2.0 installs gain their columns)
  PASS  v3: an installer-shaped install has every column the code writes
  PASS  v3: no literal route is shadowed by an earlier {id} route
  PASS  v3: the CSV import form posts to a route that reaches the importer
  PASS  v3: version is 3.1.5 everywhere (healthz, api_ping, openapi), with nothing older left behind
  PASS  v3: the bump did not fork the shared core modules
  PASS  v3: all 9 shared cores are byte-identical to the reference copy
  PASS  v3: the deploy kit ships both files, fully filled
  PASS  v3: the deploy kit names THIS product and carries the fixed healthcheck
  PASS  v3: the brand accent is applied in :root AND html[data-theme=dark]
  PASS  v3: an uploaded logo and the brand name reach the app chrome, not just the settings page
  PASS  v3: GET /mcp is routed to the unauthenticated probe (a 401 hides that we speak MCP)
  PASS  v3: the GET probe runs no key check and exposes no tools
  PASS  v3: GET reaches the core and gets 405 + Allow: POST
  PASS  v3: MCP creates requests through Revup::createRequests(), never its own INSERT
  PASS  v3: MCP cannot change the gate threshold or the review URL
  PASS  v3: exactly ONE send-window planner, used by the preview and the release
  PASS  v3: every advertised reply merge field actually resolves
  PASS  v3: an unknown placeholder is left alone rather than blanked
  PASS  v3: reply merge fields are data keys only
  PASS  v3: both backup doors use one redaction list
  PASS  v3: the redaction list names every credential column revup stores
  PASS  v3: no call to a function this product does not define
  PASS  v3: the 2.0 settings and API-key handlers are permission-gated too
  PASS  v3: every v3 write handler is permission-gated
  PASS  C2: no sensitive handler is login-only — all are role-gated
  PASS  C2: both backup doors call the ONE shared redaction list
  PASS  C2: Api::requireKey refuses a deactivated user (u.active)
  PASS  C2: every App:: method the controllers call actually exists
  PASS  C2: Api::base() does not double an already-absolute base
  PASS  nav: every sidebar link is highlighted by the page it opens
  PASS  nav: no page highlights a sidebar item that does not exist
  PASS  ip: a direct visitor's forged headers are ignored
  PASS  ip: behind a trusted proxy the forwarded visitor is used
  PASS  ip: CF-Connecting-IP wins over the X-Forwarded-For chain
  PASS  ip: the chain is read right-to-left, past trusted hops
  PASS  ip: a Cloudflare edge is not a trusted proxy by default
  PASS  ip: CIDR maths (v4 boundaries + v6, no cross-family match)

== UI kit: the send queue runs in slices with visible progress ==
  PASS  send-batch: routed, permission-gated and CSRF-checked like the single send
  PASS  send-batch: dispatches through the SHARED sender, never its own copy
  PASS  send-batch: a slice is bounded — a long queue can never become one long request
  PASS  send-batch: the progress list carries a FIRST NAME only, like the public gate
  PASS  send-batch: every run is audited with what it sent and what is left
  PASS  send-batch: the runner stops itself when nothing is moving (no infinite loop)
  PASS  send-batch/engine: the private-offer rule the queue reports is Revup's own
  PASS  send-batch/engine: a bad rating parses, it is the threshold that gates it (got 3, want 3)

== 3.1 schedule + follow-up ==
  PASS  schedule/happy: a running window with work releases once the hour has come (got '', want '')
  PASS  schedule/happy: it releases exactly AT the chosen hour, not only after it (got '', want '')
  PASS  schedule/happy: a window that missed its hour still goes out on the next pass (got '', want '')
  PASS  schedule/happy: the hour is clamped into a real day, whatever is stored
  PASS  schedule/refusal: automatic release is off unless someone turned it on (got 'Automatic release is off for this window.', want 'Automatic release is off for this window.')
  PASS  schedule/refusal: a finished window is never revived by the runner (got 'This window is not running.', want 'This window is not running.')
  PASS  schedule/refusal: before the hour, nothing goes out (got 'Not yet — this window releases at 09:00 UTC.', want 'Not yet — this window releases at 09:00 UTC.')
  PASS  schedule/refusal: nothing due means nothing sent, however loudly the cron fires (got 'Nothing is due yet.', want 'Nothing is due yet.')
  PASS  schedule/idempotent: a second pass on the same day releases nothing (got 'Already released today.', want 'Already released today.')
  PASS  schedule/idempotent: yesterday's stamp does not block today
  PASS  schedule/idempotent: the day is CLAIMED before anything is sent, so a crash cannot double-send
  PASS  followup/happy: silence for the full gap earns exactly one reminder (got '', want '')
  PASS  followup/happy: the gap counts from when the first message actually went out (got 'Too soon — 7 day(s) have not passed since it was sent.', want 'Too soon — 7 day(s) have not passed since it was sent.')
  PASS  followup/happy: the reminder reuses the SAME link — it is the same single-use ask
  PASS  followup/happy: it says once that it is a reminder, and adds nothing else
  PASS  followup/refusal: a customer who rated is never nudged again (got 'This customer already rated.', want 'This customer already rated.')
  PASS  followup/refusal: a low rating counts as an answer too — feedback is not silence (got 'This customer already rated.', want 'This customer already rated.')
  PASS  followup/refusal: someone who went to the review page has already been asked enough (got 'This customer already went to the review page.', want 'This customer already went to the review page.')
  PASS  followup/refusal: a request whose first message never left is not followed up (got 'The first message never went out.', want 'The first message never went out.')
  PASS  followup/refusal: no gap configured means no reminder, ever (got 'No follow-up is configured for this window.', want 'No follow-up is configured for this window.')
  PASS  followup/refusal: a nonsense gap is read as OFF rather than as an instruction
  PASS  followup/idempotent: exactly one reminder, whatever else runs (got 'The follow-up already went out.', want 'The follow-up already went out.')
  PASS  followup/idempotent: the stamp is written ONLY on a successful send
  PASS  followup/idempotent: a reminder never rewrites the first message's own history
  PASS  followup/idempotent: the runner only ever looks at requests with no stamp yet
  PASS  schedule/permission: both new POSTs need batch.run and a CSRF token
  PASS  schedule/permission: the cron door refuses to answer over HTTP
  PASS  schedule/permission: the public demo never runs the schedule
  PASS  schedule/permission: overlapping runs are refused by an atomic lock, not a lock file
  PASS  schedule: the runner reuses the product's OWN sender — no second send path
  PASS  schedule: a stopped runner is VISIBLE — every pass is stamped and the page reads it
  PASS  followup/refusal: manual mode is not a failure — an install with no channel is not nagged
  PASS  followup/refusal: a follow-up with no channel is NOT stamped, so it goes out once SMTP is set up
  PASS  schedule: a manual run and the cron run take the SAME path
  PASS  scope: api_keys gains a scope column defaulting to full — an upgrade never loosens
  PASS  scope: requireKey reads the key scope and refuses a write on a read-only key
  PASS  scope: mintKey persists the chosen scope, and anything but read is full access
  PASS  scope: /mcp hands the key scope to the core, which refuses write tools per tool
  PASS  scope: the key form lets an administrator mint a read-only key
  PASS  licence: every font ships the SIL OFL text for its own family beside it
  PASS  licence: LICENSE.txt is present and carries the IF WE STOP (CONTINUITY) clause
  PASS  F2 PHP: the installer refuses PHP older than 8.1 with a plain message, before anything else runs
  PASS  F2 PHP: the requirements no longer say PHP 8.0 or "PHP 8+"
  PASS  F2 security: a signed-in non-admin (viewer, member) gets 403 on every backup and restore route
  PASS  F2 security: the probe is real — an administrator is let through to the backup download
  PASS  F2 invite: with SMTP set up, the invitation is delivered over a real SMTP conversation, carrying the link
  PASS  F2 invite: with no SMTP it says so and sends nothing (the link is still shown once)
  PASS  F2 invite: a refusing mail server is reported, never thrown
  PASS  F2 invite: the invite handler emails it and still shows the link once
  PASS  F2 routes: every POST form in the views reaches a POST route (38 checked)
  PASS  F2 webhooks: the events offered in Settings (feedback.received, rating.public, request.sent) are exactly the events the code sends
  PASS  F2b ribbon: the public page /r/Ab3dEf6hIj9kLm2nOp5q says what it is ("this is the page your customers rate you on"), not "signed in as the admin"
  PASS  F2b ribbon: an admin page (/requests) keeps the admin wording
  PASS  F2b ribbon: below 600px it starts as the small "Own it" pill; a tap opens it and that choice holds for the session
  PASS  F2b ribbon: injected once, just before </body>; output with no </body> (JSON, CSV, PDF) passes untouched
  PASS  F2b ribbon: boot() installs it only behind the .demo-mode marker
  PASS  F2b security: /backup/scheduled refuses a signed-in viewer, member with no token or a wrong one (404)
  PASS  F2b backup: a value was planted and read back in every credential column (settings.smtp_pass, settings.twilio_token, settings.backup_token_hash, settings.oidc_client_secret, users.password_hash, users.totp_secret, users.totp_recovery, api_keys.token_hash, webhooks.secret, invites.token_hash)
  PASS  F2b backup: the JSON download carries none of them
  PASS  F2b backup: the scheduled backup carries none of them
  PASS  F2b backup: both backup doors use the one list, revup_backup_redact()

————————————————————————————————————————————————————
ALL 336 TESTS PASSED (0 failed)

← Back to Revup · Manual · Quickstart · API

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →