Repository navigation
1.9.79: both redirect links read a rule criterion's "all" as every visitor - #294
Merged
Merged
Conversation
…or tools
PUT /rotators/{id}/rules replaces a redirector's rule set the way Setup ›
Redirector saves it, in one transaction: every rule the redirector is to
have, in the order the redirects try them. A rule or entry sent with its id
keeps it (its clicks and report rows stay its own), one without is added,
one left out is removed (remove_rotator_rule, as the page asks). What the
page refuses is refused (a rule with no criterion or destination, two
destinations without a split test, a split test without weights), every
problem named by its place in one 422 with nothing written; another
account's campaign or another redirector's rule is refused by name.
?dry_run=1 answers the set as it would be stored and each rule's change;
the route is stageable; PATCH is a 405. A GET body sent back changes
nothing.
The rules had no order but their ids, so a rule could only be added after
the others: 202_rotator_rules gains position (the 1.9.79 rung), read by
rtr.php and offrtr.php through Prosper202\Rotator\RuleOrder (in id order on
a database the upgrade has not reached, rather than failing the click), the
Setup page, the API and both rotator reports; the page's save and POST
.../rules write it. The API's rule writers lock the redirector's row.
A criterion's values are stored as the page stores them (split, trimmed,
`all` as ALL), and a device in capitals, which never matches, is refused.
p202 rotator set-rules <id> (--rules or --file, --dry-run) sends a set; the
MCP connector gains rotator_create, rotator_update, rotator_delete and
rotator_set_rules, whose rules argument is the set as JSON with a schema
(a tool argument can now be a JSON document). Hosted AI keys may write
redirectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… tools Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
With the rotator tools the list is 84,564 characters; with #260's text-ad tools beside them, about 87,900. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… them The merged tree's unit tier read three of the new statements as hidden from it: the criteria and destinations delete named its table through a variable, the click path's rule read was built in a class the cached-read scan does not follow, and two rule reads put a method call after their WHERE. The delete names each table literally, RuleOrder makes the click path's cached read itself, and the ORDER BY goes through a variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
… back
PUT /rotators/{id}/rules?dry_run=1 built each rule's criteria and
redirects in the order the body listed them, while the write updates kept
entries in place and GET reads them back by id, so a body that reordered a
rule's entries (keeping their ids) previewed one order and wrote another.
The nested order decides nothing on the click path: every criterion has to
match, and a destination is drawn by weight. So the plan now holds each
rule's entries as the write will read them back (the kept ones by id, the
new ones after them as sent), and the preview and the write answer the
same data. The rules keep the order sent. The API reference and the
OpenAPI description say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…rule
PUT /rotators/{id}/rules/{ruleId} read and checked the rule, then took the
redirector's lock, so a rule set that removed the rule could commit in
between: the update then matched no row and its criteria and redirects
were inserted for a rule that no longer existed (these tables have no
foreign keys), answered as a success. DELETE /rotators/{id}/rules/{ruleId}
checked the rule the same way and answered a removed rule's delete as done.
Both now take the redirector's lock, then the rule's own (a locking read,
which also holds off Setup › Redirectors' save, which takes neither), before
the first read of the transaction, and read the rule under them. A rule gone
by then is a 404, with nothing written; an update that matches no row is
checked by a current read and refused the same way. The redirector delete
no longer reads the redirector before its lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
1.9.79 gained #262. The only conflict was 202-config/changelogs.json, where both sides added 1.9.79 lines: 1.9.79's lines are kept and this branch's follow them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…a rule at once
A rule write read its answer back after its transaction committed, when
the redirector's lock was already let go: a writer waiting on that lock
could replace the set in between, and PUT /rotators/{id}/rules answered the
second writer's rules beside its own changes (POST and PUT of one rule
answered the second writer's set too). The read-back now runs inside the
transaction, under the locks, and is what the write answers.
A rule set's 422 promised every problem at once, and gave one per rule:
the name, the flags and each criterion and redirect threw at the first
problem. Every check of a rule now runs and each problem is answered by
its place (rules.0.criteria.1.value), several in one entry included; the
single-rule writes report every entry's problems the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
The catalogue keeps #280's traffic source, account and category updates and deletes beside the rotator writes, and rotator_list takes #279's hosted omission of --all. Both changelogs keep the version branch's lines and add this branch's. The tool list is 89,469 characters with both; its budget is 90,000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…or_get answers #281 holds a hosted call's arguments to 1,000 JSON values, because each item of a list argument becomes a flag of its own. rotator_set_rules takes its rules as one JSON document, written to its command as one flag, and has no per-rule tool to fall back on: at 1,000 values a hosted call could send back about 30 rules of one criterion and two destinations, where rotator_get answers about 100 under the result bound. A tool that takes a JSON document is held to 5,000 values instead; at that bound the call allocated 5.5 MB, the list arguments at theirs 5.1. Past it the refusal says what to leave out and which command sends a larger set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
"set the api url" ranked p202 system integrations level with config set-url once the rotator writes and #280's tasks were both in the index: the two had scored within half a percent on the version branch, and the new commands' words moved the weights enough to tie them. config set-url now has a search task with the words the docs use for what it sets, the install's base URL or instance URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…B limit --file read the whole file with os.ReadFile before checking it against the API's 1 MB body limit, so a path naming something huge, or endless like /dev/zero, was read into memory before the size refusal could run. A path is now read through the same rotatorMaxRuleSetBytes+1 limit stdin already was, and anything past it is refused by the size check. TestRotatorSetRulesReadsAFileOnlyUpToTheLimit sends --file /dev/zero and expects the 1 MB refusal with no request; with os.ReadFile put back it dies with "fatal error: out of memory" under a 3 GB address-space limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…or-all-spelling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
…sitor rtr.php read a criterion value of ALL or all as every visitor; offrtr.php read only ALL. A rule stored as "all" (the API stored values as sent before 1.9.79, and Setup > Redirectors stores what is typed) sent a visitor to the rule from a redirector link and to the default from a landing page's offer link (measured live on both scripts). Neither read "All", " all " or "United States(US), all" (the space kept on the value) as every visitor, while p202 rotator test predicted "All" did. Prosper202\Rotator\Criterion is now the one reading: the value split on commas, each value trimmed, the empty ones dropped, and ALL in any case, alone or among other values, matching every visitor whatever the statement (is not ALL did on both scripts, and still does). Both scripts call it; so do the API's writes and the Redirectors page's save, which store ALL; the Redirectors page and Overview > Rotator Breakdown list such a criterion as "any visitor". go-cli has the same reading for p202 rotator test and p202 diff, and both trees hold to tests/fixtures/rotator-criterion-values.json, the spellings writers stored. CriterionTest holds every served file that names the criteria table to a list, and each reader to calling Criterion and spelling ALL nowhere; TestOnlyTheCriterionReaderSpellsAll does the same for the CLI. tests/live/rotator-criterion-spellings.sh clicks both scripts for each stored spelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Contributor
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8dab59b923
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Contributor
Both redirect scripts read a criterion holding ALL as every visitor whatever its type (Criterion::matchesEveryone()), and the writers held an IP value to IpCriterion::refusal() first, which refused ALL as "not an IP address". A redirector with an IP criterion of ALL could not have its rule set sent back as GET answered it: the set PUT, and the single-rule POST and PUT that share planRule(), answered 422 for a rule the links read as valid; and the Redirectors page refused the same value. RotatorsController::criterion() now returns a value holding ALL (read by Criterion::values(), so " all ", "All" and "US, ALL" as the redirects read them) before any type's own form is asked of it, so a country or device list beside ALL is not refused for its form either. The page's check and write go through Criterion::refusal() and Criterion::stored(), which hold the same reading for the page. RotatorRuleSetIntegrationTest stores an IP=ALL rule, reads it with GET and sends it back through the set PUT (unchanged) and the single-rule PUT; it failed before the fix with "Invalid rules.0.criteria.0.value". CriterionTest holds refusal() and stored() to the same spellings, and the live passes post an IP criterion of all to the page and click an IP rule stored as " All ". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in 1.9.79's new commits. The conflicts were in 202-config/changelogs.json, 202-config/functions-upgrade.php, changelogs.txt, where both sides added lines: the incoming lines are kept as they are and this branch's follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
Brings in 1.9.79-mcp-rotator-crud (this branch's stacked base), and through it 1.9.79's new commits. The Go workflow's path lists conflicted where both sides added a file a Go test reads: both are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRoS2SViuxaucFaKWsFnLr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backlog: 3C1D
What changed and why
rtr.phpread a criterion value ofALLorallas every visitor;offrtr.phpread onlyALL. So a rule stored asallsent a visitor to the rule from a redirector link and to the default from a landing page's offer link. Two writers storedall:#265 makes the API store
ALL, but rows already stored keepall.Measured before the fix on a live instance: one redirector per stored spelling, one rule to
/rule, default/default, and a Chrome click through each entry point.is ALLis allis Allis " all"is "United States(US), all"is_not allWhere the other spellings came from. The history here goes back to 1.9.29, where
rtr.phpgained|| in_array('all', $values). The first v3 rule write (fc26a2a7) stored(string) ($c['value'] ?? ''): any spelling and any spacing, and''for a criterion sent without a value.p202 rotator test(go-cli/cmd/verify.go) predictedEqualFold(TrimSpace(value), "ALL"), which is a third reading: it calledAllevery visitor, which neither script did, and missedUnited States(US), all, whichrtr.phpwould have read had the space not been there.One reading.
Prosper202\Rotator\Criterionreads a stored value as follows:ALLin any case, alone or among other values, matches every visitor, whatever the statement.is not ALLalready matched everyone on both scripts, and still does.Its readers and writers:
rtr.php,offrtr.phpCriterion::values()andmatchesEveryone(). Nothing else in the click path changed: same reads, samein_arraycomparisons per type.RotatorsController::criterion())Criterion::normalize().AllandALLnow becomeALLtoo, not onlyall.ajax/rotator.php)Criterion::normalize(); it storedallas typed.sort_rotator.php)Criterion::describe(). The Breakdown's value is no longer in bold.p202 rotator test(verify.go)go-cli/cmd/rotator_criterion.go.p202 diff(diff.go)allagainstALL, or a list with and without spaces, as a difference between two installs.Rows already stored (#25). The reader takes every spelling a writer stored, and nothing in the database is rewritten. A rule is rewritten in canonical form only when it is next saved, by the page or the API.
What this changes for stored rows.
all: offer-link clicks now go to the rule. Redirector-link clicks did already.All,all,United States(US), all: both links now send every visitor to the rule; neither did before. This is the decision a reviewer should check, below.United States(US), Canada(CA), stored with the space by the old API: now matches Canada. This is 1.9.79: redirector create, update, delete and whole-rule-set writes in the MCP connector #265's "found outside" item."None". I found no writer that stored a "none" criterion value. The nearest is
'', an empty value from the old API. It is now an empty list, soismatches no visitor andis_notevery visitor, as before, with one exception: an ISP criterionis ""matched every visitor while MaxMind ISP lookups were off (in_array(null, [''])is true). It now matches none.ClickPathReads (#27). The criteria read in both scripts is unchanged. It is a read by rule id, from the redirector's own active rules (
RuleOrder::activeRules), and no read or join was added.Docs.
documentation/api/12-rotators.md(the criterionvaluerow),docs/openapi.yaml(the criterion value, andsetRotatorRules), and both changelogs.Tests
tests/Rotator/CriterionTest(unit). Every spelling intests/fixtures/rotator-criterion-values.jsonis checked for its values, its every-visitor reading and its normalized form, and normalizing twice changes nothing. That fixture lists the 13 spellings writers stored, each with its writer.describe().202_rotator_rules_criteriais listed, so a new reader fails until it is listed. Each reader must callCriterion::and must contain no string literalALLin any case;offrtr.php's own'ALL'is how the two scripts came to differ.TestCriterionValuesReadAsThePHPTreeReadsThemreads the same fixture throughrepoPath(); the fixture is added togo-cli.yml's paths, both lists.TestRotatorDestinationReadsEverySpellingOfAll.TestDiffComparesCriteriaAsTheRedirectsReadThem.TestOnlyTheCriterionReaderSpellsAll, an AST scan that refuses a literalALL/Alloutsiderotator_criterion.go. Lower-case"all"names the--allflags and is not looked for.tests/live/rotator-criterion-spellings.sh(new). One redirector per stored spelling, written into the row as an older writer left it. A click goes throughrtr.phpand one throughoffrtr.phpfor each, and every spelling sends the visitor to the rule; a value that only resembles ALL (Mall) does not.tests/live/setup-pages.sh: the page saves a criterion typedallasALLand lists the rule as "If any visitor".RotatorRuleSetIntegrationTest::testValuesAreStoredAsThePageStoresThemnow also coversAllandChrome, aLL;IpCriterionTest, which pins the page's write statement, now pins the new one.Plants: 11, each caught by a named test. Backups were
mktempcopies checked withcmp, the tree was restored, and the run was repeated on the final tree.rtr.phpwith its ownALL/allcheck again. Caught byCriterionTestand the spellings pass (All,all, the list).offrtr.phpwith its ownALLcheck again. Same tests,allincluded.Criterioncase-sensitive.Criterionnot trimming.allasALL. Caught by the integration test andCriterionTest.setup-pages.sh).setup-pages.shandCriterionTest).rotator testback to its whole-valueEqualFold.diffcomparing raw values.Live pass
Instance. A local MariaDB 10.11 / PHP 8.3.6 instance on this branch, port 8291. It was installed by running
install.php's own steps without the licence form; the licence stub is down.The licence check was made unreachable. For the signed-in pages, the server's proxy points at a closed port, which
AUTH::is_valid_api_key()treats as a network failure and lets through.What could not run here. The Go CLI answers 402, so
p202 rotator testandp202 diffran only in Go unit tests.Results.
tests/live/rotator-criterion-spellings.sh: 21 passed, 0 failed. Every stored spelling sends the visitor to the rule through bothrtr.phpandoffrtr.php;Mallgoes to the default on both.Mallas "If Country is Mall", and the browser rules as before.tests/live/setup-pages.sh: 331 passed, 0 failed (1.9.79: redirector create, update, delete and whole-rule-set writes in the MCP connector #265 reported 328).tests/live/offer-rotator-routing.sh: 30/0.tests/live/rotator-breakdown.sh: 10/0.setup-pages.shTRUNCATEs the rotator tables and leaves202_clicks_rotator, so a reused instance hands new redirectors ids that old clicks name.AUTO_INCREMENTwas moved past them (see "Found outside").Verification
.claude/skills/p202-verify/scripts/verify.sh --since origin/1.9.79 --changed, run on the final tree before the commit (everything staged), PHP 8.3.6 with the fullvendor/, schema tier on a scratch MariaDB 10.11 database. The unit tier ran 6119 tests, 4 skipped (the documented "User class requires DB singleton" ones). The live instance's202-config.phpwas moved out of the checkout for the run.Then actionlint v1.7.7 was built into a scratch directory (
go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7), andverify.sh --tier actionlintwas run with it onPATH:actionlint PASS.CI status: not yet observed.
What could not run here
rotator testanddiffran only in Go unit tests.For the reviewer to check
ALL/all. A rule stored asAll,allorUnited States(US), allmatched no visitor on either link and now matches every visitor on both. A storedUnited States(US), Canada(CA)now matches Canada.p202 rotator testalready told usersAllmeant every visitor; and the writers now store these spellings asALL, so reading them as anything else would make a saved-again rule change meaning.allshould change, makeCriterion::isEveryone()compareALLandallexactly, and drop the trimming invalues(). The fixture's cases say which rows move.is not ALLstays "every visitor". That is what both scripts did, and the docs now say it.all→ALL), so the editor showsALLafter a save. It already trimmed in its JavaScript.Criterion::describe(), where the value was in<strong>.Review follow-up: ALL on an IP criterion (9I0R, 3de05fa)
This answers Codex's P2 on
RotatorsController.php("Allow ALL in IP criteria"). It is the defect this PR first listed under "Found outside".Both links read a criterion holding ALL as every visitor, whatever its type. The writers, though, checked an IP value with
IpCriterion::refusal()first, which refusedALLas "not an IP address". So the following could not be written, nor could a stored IP=ALL rule be sent back as GET answered it:planRule()→ruleParts()→criterion();The changes:
criterion()returns a value holding ALL before any type's own check is made. It reads the value withCriterion::values()andmatchesEveryone(), the same reading the redirects use, soall,All,203.0.113.7, ALLandUS, ALLare taken on any type and stored with ALL asALL. A country or device list beside ALL is no longer refused for its form either.Criterion::refusal()andCriterion::stored().RotatorRuleSetIntegrationTest::testAValueHoldingAllIsTakenWhateverTheTypestores an IP=ALL rule, reads it with GET and sends it back through the set PUT (answered unchanged, rows untouched) and the single-rule PUT. Before the fix it failed withInvalid rules.0.criteria.0.value.CriterionTestholdsrefusal()andstored()to the same spellings.Criterion::refusal()ALL handling removed: caught byCriterionTestand the page pass.Criterion::stored()ALL handling removed: caught byCriterionTest.setup-pages.sh333/0; it posts an IP criterion ofalland checks thatip|ALLis stored.Allon both links.offer-rotator-routing.sh30/0 androtator-breakdown.sh10/0.Allrule went through GET and then PUT twice, answering 200 both times.criterion()throughplanRule(), and a trial merge of the two branches has the fix on both paths. The conflicts are still only the two changelogs andsetup-pages.sh.Found outside this item (one line each)
rtr.phpcompares an ISP criterion with the first comma-part ofgetIspData(),offrtr.phpwith the whole answer, so an ISP rule can match on one entry point and not the other.p202 rotator testreads a bare country code (US) as that country, while the redirects never match one (countryCodeFromValue).tests/live/setup-pages.shTRUNCATEs the rotator tables but not202_clicks_rotator, so on a reused instance new redirectors get ids old clicks name, andtests/live/rotator-breakdown.shthen counts those clicks.Generated by Claude Code