Skip to content

8 October 2026

5 changes:

  • The administration API answers to administrators, and the search box to everybody
  • A profile picture a user can set
  • A profile picture is always stored as JPEG, and WebP is accepted
  • A profile picture from Gravatar, on the user's request
  • A phone photo is turned upright

The administration API answers to administrators, and the search box to everybody

Reported by a consuming project: with OrganizationScope set, GET /admin/search answered 403 to everybody. Looking at why showed something worse. ApiAdmin used the rule that generated CRUD endpoints use: no grant is no opinion, and no opinion lets the call through. So in an installation without organisations, any signed-in account could read /admin/users (the user list, with addresses), /admin/logs and /admin/summary. With an organisation in scope, nobody could, administrators included.

Fixed

  • users, logs and summary are decided like the screens they serve. AdminAccess checks admin.users, admin.logs and admin.dashboard at the administration floor, or by grant under admin_access = permissions. A permission-store grant for the action itself still opens it.
  • search needs only a signed-in user, in an organisation too. A search source with no permission shows only to administrators. It used to show to everybody the endpoint let in, which once anyone may ask would have included the scaffolded Users source. The MCP search tool reads the same registry and gets the same rule.
  • The scaffolded app/search.php says how to open a source to everybody.

Documentation

  • Search Guide: Permissions. API Guide: who may call the administration endpoints.
  • Upgrade Guide: two rows.

A profile picture a user can set

User had $photo (a media usage) and $avatarurl, and /me returned avatarurl. But no screen set either one, nothing turned photo into a URL, and avatarurl is not a column, so it was always the defaultAvatarUrl setting. That setting is empty unless set. Asked for by a consuming project.

Added

  • A Profile Picture card on Account → Profile, in all three themes. It uploads a JPEG, PNG or GIF, and Remove takes the picture away. Account::profilephoto() is a write action, so it needs a POST with the session's token.
  • Pramnos\User\ProfilePhoto.
  • set() stores the upload through Media as a 256-pixel square crop, records the usage in users.photo, and releases the picture it replaces.
  • remove() takes the picture away, and url() gives its absolute URL.
  • adoptFromProvider() copies an SSO provider's picture for a user who has none, when profile_photo_from_provider is on. The copy goes through OutboundUrl.
  • release() gives a picture's usage back.
  • User::load() sets avatarurl from the picture, so /me, the admin user list and every screen that reads it show the picture. defaultAvatarUrl applies only when there is no picture.
  • The OIDC picture claim carries the picture. It used to read an avatarurl column that does not exist, so it was always null.
  • Erasing an account releases its picture, and the data export includes its URL.
  • Testing\Schema recipes for media and mediause.

Fixed

  • Uploading an image that Media already has no longer leaves the new upload on disk. The record points at the existing copy, and the second file was never deleted, so every repeated upload left one more orphan under www/uploads.

Documentation

  • Account Guide: "Profile picture". OAuth Client Guide: copying the provider's picture in the callback.

A profile picture is always stored as JPEG, and WebP is accepted

Following the entry above: the picture kept the upload's format, so a PNG stayed a PNG and a GIF became a PNG. WebP was refused, because Media's image engine does not read it.

Changed

  • ProfilePhoto converts every picture to JPEG before Media sees it:
  • JPEG, PNG, GIF or WebP are accepted, judged by the file's bytes and not by the name or the type the browser sent;
  • transparency is flattened onto white;
  • the longer side is at most 1024 pixels;
  • the 256-pixel square is cut from that JPEG.
  • A provider's picture goes through the same conversion. adoptFromProvider() takes an optional fetcher in place of the media object it took before. The default fetcher is OutboundUrl.
  • A file over 10 MB, or one whose header declares more than 40 million pixels, is refused before it is decoded. A few bytes can announce an image that would take gigabytes to decode.
  • Uploads are read only from a file PHP received in the request (is_uploaded_file()), so a forged tmp_name cannot read another file on the server.
  • The upload field accepts image/webp in all three themes.

Documentation

  • Account Guide: the formats, the limits and the orientation caveat.

A profile picture from Gravatar, on the user's request

Gravatar was left out of the profile picture because a lookup sends a hash of the address to a third party. As a button the user presses, it sends only that person's hash, only when they choose to.

Added

  • A Use my Gravatar button on the Profile Picture card, in all three themes, with a line that says what it does. Account::profilephoto() takes it as gravatar.
  • ProfilePhoto::fromGravatar($user).
  • It asks https://www.gravatar.com/avatar/<sha256 of the trimmed, lowercased address> with d=404, fetched by the server through OutboundUrl.
  • It stores the result as JPEG, like an upload.
  • A 404 is reported as "no Gravatar picture", not stored as the placeholder.
  • profile_photo_gravatar = 0 hides the button. It is shown unless set to off.

Documentation

  • Account Guide: "From Gravatar, when the user asks".

A phone photo is turned upright

A phone stores the sensor's pixels and records the turn in an EXIF tag. Re-encoding to JPEG dropped the tag, so a portrait photo could show sideways.

Fixed

  • ProfilePhoto reads the orientation of a JPEG and turns the image upright before storing it, when PHP has the exif extension. Without the extension the picture is stored as the camera wrote it, as before. The turn itself is ProfilePhoto::orient($image, $orientation) and covers the eight cases of the specification.

Documentation

  • Account Guide: the orientation sentence.