PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / 2.5.1
Forumax – AI Powered Advanced Community Forum Plugin v2.5.1
2.5.1 2.5.0 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 trunk 1.0.8 1.1.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 All 31 releases
bbp-core / includes / features / frmx-profile-avatar.php

frmx-profile-avatar.php in Forumax – AI Powered Advanced Community Forum Plugin 2.5.1, at includes/features/frmx-profile-avatar.php

405 lines 12.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Custom Profile Avatar Upload
4 *
5 * Allows forum users to upload their own profile avatar
6 * from the bbPress user profile edit page.
7 *
8 * @package Forumax
9 * @since 2.2.0
10 */
11
12 defined( 'ABSPATH' ) || exit;
13
14 /**
15 * Class Frmx_Profile_Avatar
16 *
17 * Handles uploading, saving, deleting and displaying
18 * custom user avatars on the front-end profile edit page.
19 */
20 class Frmx_Profile_Avatar {
21
22 /**
23 * User-meta key used to store the custom avatar attachment ID.
24 *
25 * @var string
26 */
27 const META_KEY = 'frmx_profile_avatar';
28
29 /**
30 * Attachment meta marking an image as a user's uploaded avatar (value: user ID).
31 *
32 * @var string
33 */
34 const OWNER_META_KEY = '_frmx_avatar_owner';
35
36 /**
37 * Register all hooks.
38 */
39 public function __construct() {
40 // Render the avatar upload field before the Name fieldset.
41 add_action( 'bbp_user_edit_before', array( $this, 'render_avatar_upload_field' ) );
42
43 // Process the uploaded avatar when the profile is saved.
44 add_action( 'personal_options_update', array( $this, 'save_avatar' ) );
45 add_action( 'edit_user_profile_update', array( $this, 'save_avatar' ) );
46 add_action( 'bbp_profile_update', array( $this, 'save_avatar' ) );
47
48 // Override the default Gravatar with the custom avatar.
49 add_filter( 'pre_get_avatar_data', array( $this, 'filter_avatar_data' ), 10, 2 );
50
51 // Handle AJAX avatar upload.
52 add_action( 'wp_ajax_frmx_upload_avatar', array( $this, 'ajax_upload_avatar' ) );
53
54 // Handle AJAX avatar removal.
55 add_action( 'wp_ajax_frmx_remove_avatar', array( $this, 'ajax_remove_avatar' ) );
56
57 // Enqueue scripts on the user edit page.
58 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
59 }
60
61 /**
62 * Enqueue avatar upload scripts and styles on the front-end user edit page.
63 *
64 * @return void
65 */
66 public function enqueue_scripts() {
67 if ( ! bbp_is_single_user_edit() ) {
68 return;
69 }
70
71 wp_enqueue_script(
72 'frmx-avatar-upload',
73 FORUMAX_FRONT_ASS . 'js/avatar-upload.js',
74 array( 'jquery' ),
75 FORUMAX_VERSION,
76 true
77 );
78
79 wp_localize_script( 'frmx-avatar-upload', 'frmx_avatar', array(
80 'ajax_url' => admin_url( 'admin-ajax.php' ),
81 'nonce' => wp_create_nonce( 'frmx_avatar_nonce' ),
82 'max_size' => 2 * MB_IN_BYTES,
83 'max_size_text' => esc_html__( 'Maximum file size: 2 MB', 'forumax' ),
84 'uploading' => esc_html__( 'Uploading…', 'forumax' ),
85 'removing' => esc_html__( 'Removing…', 'forumax' ),
86 'error' => esc_html__( 'Something went wrong. Please try again.', 'forumax' ),
87 'confirm' => esc_html__( 'Are you sure you want to remove your profile photo?', 'forumax' ),
88 ) );
89 }
90
91 /**
92 * Render the avatar upload section on the user edit form.
93 *
94 * @return void
95 */
96 public function render_avatar_upload_field() {
97 $user_id = bbp_get_displayed_user_id();
98 $avatar_id = (int) get_user_meta( $user_id, self::META_KEY, true );
99 $avatar_url = $avatar_id ? wp_get_attachment_image_url( $avatar_id, 'thumbnail' ) : '';
100 $has_avatar = ! empty( $avatar_url );
101
102 // Fall-back to Gravatar when no custom avatar is set.
103 if ( ! $has_avatar ) {
104 $avatar_url = get_avatar_url( $user_id, array( 'size' => 150 ) );
105 }
106 ?>
107 <fieldset class="bbp-form frmx-avatar-fieldset">
108 <legend><?php esc_html_e( 'Profile Photo', 'forumax' ); ?></legend>
109
110 <div class="frmx-avatar-upload-wrap">
111 <div class="frmx-avatar-preview" id="frmx-avatar-preview">
112 <img
113 src="<?php echo esc_url( $avatar_url ); ?>"
114 alt="<?php esc_attr_e( 'Profile Photo', 'forumax' ); ?>"
115 id="frmx-avatar-img"
116 />
117 <?php if ( $has_avatar ) : ?>
118 <button type="button" class="frmx-avatar-remove" id="frmx-avatar-remove" title="<?php esc_attr_e( 'Remove Photo', 'forumax' ); ?>">
119 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
120 </button>
121 <?php endif; ?>
122 </div>
123
124 <div class="frmx-avatar-actions">
125 <label for="frmx-avatar-input" class="frmx-avatar-upload-btn">
126 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
127 <?php esc_html_e( 'Upload Photo', 'forumax' ); ?>
128 </label>
129 <input
130 type="file"
131 id="frmx-avatar-input"
132 name="frmx_avatar_file"
133 accept="image/jpeg,image/png,image/gif,image/webp"
134 class="frmx-avatar-file-input"
135 />
136 <p class="frmx-avatar-hint">
137 <?php esc_html_e( 'JPG, PNG, GIF or WebP. Max 2 MB.', 'forumax' ); ?>
138 </p>
139 <div class="frmx-avatar-status" id="frmx-avatar-status"></div>
140 </div>
141 </div>
142
143 <input type="hidden" name="frmx_avatar_id" id="frmx-avatar-id" value="<?php echo esc_attr( $avatar_id ); ?>" />
144 <?php wp_nonce_field( 'frmx_avatar_nonce', 'frmx_avatar_nonce_field' ); ?>
145 </fieldset>
146 <?php
147 }
148
149 /**
150 * Handle avatar upload via AJAX.
151 *
152 * @return void
153 */
154 public function ajax_upload_avatar() {
155 check_ajax_referer( 'frmx_avatar_nonce', 'nonce' );
156
157 if ( ! is_user_logged_in() ) {
158 wp_send_json_error( array( 'message' => esc_html__( 'You must be logged in.', 'forumax' ) ) );
159 }
160
161 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : get_current_user_id();
162 if ( $user_id <= 0 ) {
163 $user_id = get_current_user_id();
164 }
165
166 // Security: only allow users to update their own avatar or admins to update any.
167 if ( $user_id !== get_current_user_id() && ! current_user_can( 'edit_users' ) ) {
168 wp_send_json_error( array( 'message' => esc_html__( 'Permission denied.', 'forumax' ) ) );
169 }
170
171 if ( empty( $_FILES['avatar_file'] ) ) {
172 wp_send_json_error( array( 'message' => esc_html__( 'No file was uploaded.', 'forumax' ) ) );
173 }
174
175 // Validate file type.
176 $allowed_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
177 $file_type = isset( $_FILES['avatar_file']['type'] ) ? sanitize_mime_type( $_FILES['avatar_file']['type'] ) : '';
178
179 if ( ! in_array( $file_type, $allowed_types, true ) ) {
180 wp_send_json_error( array( 'message' => esc_html__( 'Invalid file type. Please upload JPG, PNG, GIF or WebP.', 'forumax' ) ) );
181 }
182
183 // Validate file size (2 MB).
184 $file_size = isset( $_FILES['avatar_file']['size'] ) ? absint( $_FILES['avatar_file']['size'] ) : 0;
185 if ( $file_size > 2 * MB_IN_BYTES ) {
186 wp_send_json_error( array( 'message' => esc_html__( 'File too large. Maximum size is 2 MB.', 'forumax' ) ) );
187 }
188
189 // Include WordPress upload handling.
190 if ( ! function_exists( 'wp_handle_upload' ) ) {
191 require_once ABSPATH . 'wp-admin/includes/file.php';
192 }
193 if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
194 require_once ABSPATH . 'wp-admin/includes/image.php';
195 }
196 if ( ! function_exists( 'media_handle_upload' ) ) {
197 require_once ABSPATH . 'wp-admin/includes/media.php';
198 }
199
200 $attachment_id = media_handle_upload( 'avatar_file', 0 );
201
202 if ( is_wp_error( $attachment_id ) ) {
203 wp_send_json_error( array( 'message' => $attachment_id->get_error_message() ) );
204 }
205
206 update_post_meta( $attachment_id, self::OWNER_META_KEY, $user_id );
207
208 // Delete the old avatar attachment if exists.
209 $this->delete_avatar_file( $user_id, (int) get_user_meta( $user_id, self::META_KEY, true ) );
210
211 // Save the new avatar.
212 update_user_meta( $user_id, self::META_KEY, $attachment_id );
213
214 $avatar_url = wp_get_attachment_image_url( $attachment_id, 'thumbnail' );
215
216 wp_send_json_success( array(
217 'avatar_url' => esc_url( $avatar_url ),
218 'attachment_id' => $attachment_id,
219 'message' => esc_html__( 'Profile photo updated successfully.', 'forumax' ),
220 ) );
221 }
222
223 /**
224 * Handle avatar removal via AJAX.
225 *
226 * @return void
227 */
228 public function ajax_remove_avatar() {
229 check_ajax_referer( 'frmx_avatar_nonce', 'nonce' );
230
231 if ( ! is_user_logged_in() ) {
232 wp_send_json_error( array( 'message' => esc_html__( 'You must be logged in.', 'forumax' ) ) );
233 }
234
235 $user_id = isset( $_POST['user_id'] ) ? absint( $_POST['user_id'] ) : get_current_user_id();
236 if ( $user_id <= 0 ) {
237 $user_id = get_current_user_id();
238 }
239
240 // Security: only allow users to remove their own avatar or admins to remove any.
241 if ( $user_id !== get_current_user_id() && ! current_user_can( 'edit_users' ) ) {
242 wp_send_json_error( array( 'message' => esc_html__( 'Permission denied.', 'forumax' ) ) );
243 }
244
245 $avatar_id = (int) get_user_meta( $user_id, self::META_KEY, true );
246
247 if ( $avatar_id ) {
248 $this->delete_avatar_file( $user_id, $avatar_id );
249 delete_user_meta( $user_id, self::META_KEY );
250 }
251
252 // Return the default Gravatar URL.
253 $default_gravatar = get_avatar_url( $user_id, array( 'size' => 150 ) );
254
255 wp_send_json_success( array(
256 'avatar_url' => esc_url( $default_gravatar ),
257 'message' => esc_html__( 'Profile photo removed.', 'forumax' ),
258 ) );
259 }
260
261 /**
262 * Save avatar on normal (non-AJAX) form submission.
263 *
264 * @param int $user_id The user ID being updated.
265 * @return void
266 */
267 public function save_avatar( $user_id ) {
268 // Verify nonce.
269 if ( ! isset( $_POST['frmx_avatar_nonce_field'] ) ||
270 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['frmx_avatar_nonce_field'] ) ), 'frmx_avatar_nonce' )
271 ) {
272 return;
273 }
274
275 // Save the hidden input value (set via AJAX).
276 if ( isset( $_POST['frmx_avatar_id'] ) ) {
277 $avatar_id = absint( $_POST['frmx_avatar_id'] );
278
279 if ( $avatar_id ) {
280 // Only accept an avatar uploaded for this user, not any media ID.
281 if ( $this->is_user_avatar( $user_id, $avatar_id ) ) {
282 update_user_meta( $user_id, self::META_KEY, $avatar_id );
283 }
284 } else {
285 delete_user_meta( $user_id, self::META_KEY );
286 }
287 }
288 }
289
290 /**
291 * Whether an attachment was uploaded as this user's avatar.
292 *
293 * Older avatars (before the owner meta existed) are accepted when they
294 * are the user's current avatar.
295 *
296 * @param int $user_id User ID.
297 * @param int $attachment_id Attachment ID.
298 * @return bool
299 */
300 private function is_user_avatar( $user_id, $attachment_id ) {
301 if ( ! $attachment_id || ! wp_attachment_is_image( $attachment_id ) ) {
302 return false;
303 }
304
305 $owner = get_post_meta( $attachment_id, self::OWNER_META_KEY, true );
306 if ( '' !== $owner ) {
307 return (int) $owner === (int) $user_id;
308 }
309
310 return (int) get_user_meta( $user_id, self::META_KEY, true ) === (int) $attachment_id;
311 }
312
313 /**
314 * Delete a user's previous avatar file, never other media.
315 *
316 * @param int $user_id User ID.
317 * @param int $attachment_id Attachment ID.
318 * @return void
319 */
320 private function delete_avatar_file( $user_id, $attachment_id ) {
321 if ( ! $attachment_id ) {
322 return;
323 }
324
325 $owner = get_post_meta( $attachment_id, self::OWNER_META_KEY, true );
326
327 // Avatars saved before the owner meta existed: the user uploaded them themselves.
328 if ( '' === $owner ) {
329 $owner = (int) get_post_field( 'post_author', $attachment_id );
330 }
331
332 if ( (int) $owner === (int) $user_id ) {
333 wp_delete_attachment( $attachment_id, true );
334 }
335 }
336
337 /**
338 * Filter avatar data to use the custom uploaded avatar.
339 *
340 * @param array $args Avatar data arguments.
341 * @param mixed $id_or_email User identifier (ID, email, WP_User, WP_Comment).
342 * @return array Modified avatar data.
343 */
344 public function filter_avatar_data( $args, $id_or_email ) {
345 $user_id = $this->get_user_id( $id_or_email );
346
347 if ( ! $user_id ) {
348 return $args;
349 }
350
351 $avatar_id = (int) get_user_meta( $user_id, self::META_KEY, true );
352
353 if ( ! $avatar_id ) {
354 return $args;
355 }
356
357 $avatar_url = wp_get_attachment_image_url( $avatar_id, 'thumbnail' );
358
359 if ( $avatar_url ) {
360 $args['url'] = $avatar_url;
361 $args['found_avatar'] = true;
362 }
363
364 return $args;
365 }
366
367 /**
368 * Extract user ID from the various formats passed to get_avatar.
369 *
370 * @param mixed $id_or_email Accepts user ID, email, WP_User or WP_Comment.
371 * @return int|false User ID or false on failure.
372 */
373 private function get_user_id( $id_or_email ) {
374 if ( is_numeric( $id_or_email ) ) {
375 return absint( $id_or_email );
376 }
377
378 if ( is_string( $id_or_email ) ) {
379 $user = get_user_by( 'email', $id_or_email );
380 return $user ? $user->ID : false;
381 }
382
383 if ( $id_or_email instanceof \WP_User ) {
384 return $id_or_email->ID;
385 }
386
387 if ( $id_or_email instanceof \WP_Post ) {
388 return (int) $id_or_email->post_author;
389 }
390
391 if ( $id_or_email instanceof \WP_Comment ) {
392 if ( ! empty( $id_or_email->user_id ) ) {
393 return (int) $id_or_email->user_id;
394 }
395 $user = get_user_by( 'email', $id_or_email->comment_author_email );
396 return $user ? $user->ID : false;
397 }
398
399 return false;
400 }
401 }
402
403 // Initialize the profile avatar feature.
404 new Frmx_Profile_Avatar();
405