Skip to content

Stop the API accepting a link expiry it discards - #812

Merged
blaipr merged 1 commit into
mainfrom
fix/the-link-expiry-is-not-the-callers-to-choose
Aug 18, 2026
Merged

Stop the API accepting a link expiry it discards#812
blaipr merged 1 commit into
mainfrom
fix/the-link-expiry-is-not-the-callers-to-choose

Conversation

@blaipr

@blaipr blaipr commented Aug 17, 2026

Copy link
Copy Markdown
Member

The API let a caller set a public link's expiry and view limit, discarded both, and confirmed them anyway.

requested:   dateExpire=1787003922   maxCountViews=7
stored:      dateExpire=1786992179   maxCountViews=3     <- configuration
echoed back: 1787003922, 7                               <- never stored

PublicLink::buildPublicLink() sets both from configuration on the way to the database, so the two parameters never did anything. What made that more than dead weight is the response: it echoed the request back, so a caller asking for a link that expired at a particular time was told it would. For a feature that hands an account's password to whoever holds the URL, the expiry is the wrong thing to be wrong about.

Why they are removed rather than honoured

getPublinksMaxTime() and getPublinksMaxViews() are the administrator's configuration, and the first is a maximum. A caller who could set the expiry could outlive it, so implementing the documented behaviour would have turned a limit into a suggestion.

Neither web path offers a way to set either — SaveEditController explicitly preserves both across an edit — so this is policy rather than preference, and the help should not have advertised it.

The response now reports what was stored, by reading the created link back. That is the pattern Account\CreateController and AuthToken\CreateController already use; this endpoint was the one that echoed its input instead.

Not changed

The response still reports hash: null, so a caller cannot build the shareable URL without a second view call. Returning the stored model would fix that, but it would also hand PublicLink.data — the sealed credential payload — to a token scoped only to PUBLICLINK_CREATE. Worth doing deliberately, with the field list chosen, rather than as a side effect here.

Tests

One test, asserting both halves: that the caller's values are not what the row holds, and that the response describes the link that exists. The second is the half that misled anyone, and it is the one that fails when the fix is reverted — the first would pass against the old code too, since the values were discarded either way.

ApiHelpMatchesControllersTest keeps the two sides honest: removing the parameters from the help without removing the reads, or the reverse, fails it.

3965 unit tests pass; PHPStan level 6 on src and PHPCS clean.

Creating a public link over the API accepted `dateExpire` and `maxCountViews`,
advertised both in its own help, and threw both away:

    requested:   dateExpire=1787003922   maxCountViews=7
    stored:      dateExpire=1786992179   maxCountViews=3   <- configuration
    echoed back: 1787003922, 7                             <- never stored

`PublicLink::buildPublicLink()` sets both from configuration on the way to the
database, so neither parameter ever did anything. What made that more than dead
weight is the response, which echoed the request back: a caller asking for a
link that expired at a particular time was told it would. For a feature that
hands an account's password to whoever holds the URL, the expiry is the wrong
thing to be wrong about.

They are removed rather than honoured because `getPublinksMaxTime()` is the
administrator's **maximum**, and a caller who could set the expiry could outlive
it — implementing the documented behaviour would have turned a limit into a
suggestion. Neither web path offers a way to set either; `SaveEditController`
preserves both across an edit.

The response now reports what was stored, by reading the created link back,
which is what `Account\CreateController` and `AuthToken\CreateController`
already do. This endpoint was the one echoing its input.

The test asserts both halves. Only the second — that the answer describes the
link that exists — fails without the fix; the first would pass against the old
code too, since the values were discarded either way.
@blaipr
blaipr merged commit d69ddbf into main Aug 18, 2026
8 checks passed
@blaipr
blaipr deleted the fix/the-link-expiry-is-not-the-callers-to-choose branch August 18, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant