fix(review): harden photo reorder against data loss + failure-path drift
Addresses ce-code-review findings on the photo-editor media-API work: - P0 (#1): PhotoRenumberer now renumbers EVERY on-disk image, using the client manifest only as preferred ORDER and appending any omitted image at the end. A stale/incomplete `order` (e.g. a second browser tab) previously left an unlisted photo at a target slot for phase-2's rename() to silently overwrite — verified data loss, now impossible. The reorder route inherits the guard; create/reconcile is unchanged. - P2 (#3): unique per-call token in the .reorder-tmp-* name so two concurrent renumbers on one folder can't collide and clobber bytes. - P3 (#7): de-duplicate the manifest so a repeated name can't shift/drop a photo. - P2 (#2): applyReorder + doDelete split the two failure stages — a failed refresh AFTER a committed reorder/delete no longer reverts to a stale or ghost state, it reconciles to disk. A DELETE 404 is treated as success so a retried ghost cell converges. - P2 (#4): both custom routes call requirePermission('api.pages.write') so the GHSA-x7hm API-key scope cap applies (owner already holds it, so the owner-only behaviour is unchanged). - P3 (#8): refresh stale comments (photo-01..NN; drop editLoadPhotos ref). PhotoRenumberer's 7-case unit suite still passes and the data-loss repro now preserves all bytes. Assets rebuilt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,11 @@ class EntryActionsApiController extends AbstractApiController
|
||||
{
|
||||
// Authenticated OWNER only (KTD8). getUser() throws 401 for anonymous.
|
||||
$user = $this->getUser($request);
|
||||
// Enforce the API-key scope cap (GHSA-x7hm) with the SAME permission the
|
||||
// stock media/page-write endpoints require. The owner already holds it
|
||||
// (their add/delete media uploads pass it), so this only caps a scoped
|
||||
// key — it never blocks the legitimate owner.
|
||||
$this->requirePermission($request, 'api.pages.write');
|
||||
if (!EntryScopeGuard::isOwnerUser($this->grav, $user)) {
|
||||
throw new ForbiddenException('Only the site owner can delete journal entries.');
|
||||
}
|
||||
@@ -73,12 +78,16 @@ class EntryActionsApiController extends AbstractApiController
|
||||
* Filename safety is defence in depth: unsafe segments (containing '/' or '..')
|
||||
* are dropped here, and PhotoRenumberer only ever renames files that already
|
||||
* exist as image media in the folder — so a crafted order body can never touch
|
||||
* the entry .md, a .gpx or a .meta.yaml sidecar.
|
||||
* the entry .md, a .gpx or a .meta.yaml sidecar. An incomplete `order` (e.g. a
|
||||
* stale second tab) is safe too: PhotoRenumberer renumbers every on-disk image,
|
||||
* appending any the manifest omits, so no photo is lost — `order` only sorts.
|
||||
*/
|
||||
public function reorderPhotos(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Authenticated OWNER only (KTD8). getUser() throws 401 for anonymous.
|
||||
$user = $this->getUser($request);
|
||||
// Enforce the API-key scope cap (GHSA-x7hm) — see deleteEntry above.
|
||||
$this->requirePermission($request, 'api.pages.write');
|
||||
if (!EntryScopeGuard::isOwnerUser($this->grav, $user)) {
|
||||
throw new ForbiddenException('Only the site owner can reorder entry photos.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user