Member photos
A member's photo is Craft's own User.photo — the same asset the control panel shows. Memberz does
not keep a second one. What it adds is an upload that is safe to expose to whoever can register.
What an upload is checked against
In this order, because the order is the point:
- Size, against the Largest upload setting. Cheapest check first. Your server's own
upload_max_filesizestill applies on top. - Extension, against
jpg,jpeg,png,gif,webp. - What the bytes actually are. An extension is a claim made by whoever uploaded the file.
image/svg+xmlrenamed to.pngis a script that runs on the profile page of everyone who views it. The file's header is read, and anything that is not a raster image of a supported type is refused whatever it is called.
Only once the bytes have been read as an image is anything written anywhere.
The accepted file is then re-encoded and scaled to fit Resize down to on its long edge. The re-encode is the part that matters as much as the checks: whatever was riding along in the original — EXIF, a comment block, bytes appended after the image data — does not survive being decoded and written back out.
Storage is Craft's saveUserPhoto(), into the volume Craft's own user settings name for user
photos. Memberz has no volume setting of its own; Craft already has that one, and a second would
only be a place for the two to disagree.
If no volume is chosen there, uploads are refused with a message saying uploads are not set up yet, and a warning is logged naming the setting. That is better than the generic failure the member would otherwise get, which sends them off resizing a file that was fine.
When there is no photo
Memberz draws something. It does not fetch anything.
The usual answer for this is Gravatar, and it is a bad trade: it sends the email address of every member on the page to a third party. Hashed, but a hash of an address is perfectly re-identifiable for any address anyone cares to guess, and a directory page is a list of them.
So the fallback is generated on your server and returned as a data URI:
- Initials — drawn from the member's own name, on a background coloured deterministically from their UID. The same member is the same colour on every page and across every deployment. From the UID rather than the name, so changing a display name does not silently repaint them.
- Silhouette — the same, without the letters.
- Nothing — no image element at all.
In templates
{{ craft.memberz.avatar(member, 96) }} {# a complete <img> #}
{{ craft.memberz.avatarUrl(member, 96) }} {# the URL, photo or fallback #}
{{ craft.memberz.initials(member) }}
Photos are requested at twice the size you ask for and cropped square from the centre, so they are not soft on a retina screen; the element carries the size you asked for.
The upload control
A photo is not saved with the rest of a profile form. It posts to its own action, because it
has to be validated before anything is written, and because the <form> around a Memberz form is
already a form — nesting another inside it corrupts both.
The shipped control posts with fetch and needs the optional runtime:
{{ craft.memberz.form('profile') }}
{{ craft.memberz.js() }}
Without the runtime the file input still renders and is simply inert; the rest of the form saves
normally. To wire it up yourself, post to memberz/account/upload-photo with the file as photo,
and to memberz/account/delete-photo to remove one. Both act on the signed-in member and take no
user id — there is no id to tamper with.
A rejected upload answers 400 with a message written for the member ("That image is too large.
The limit is 4 MB."). Any other failure is logged and answered generically: a stack trace or a file
path on a public page tells an attacker more than it tells the member.
Setting Let members upload their own photo off refuses uploads at the service, not just in the template — a form posted directly is refused too.