diff --git src/bp-core/bp-core-avatars.php src/bp-core/bp-core-avatars.php
index c607bb2..103ce22 100644
--- src/bp-core/bp-core-avatars.php
+++ src/bp-core/bp-core-avatars.php
@@ -564,8 +564,9 @@ function bp_core_delete_existing_avatar( $args = '' ) {
  * @see bp_core_check_avatar_upload()
  * @see bp_core_check_avatar_type()
  *
- * @param array $file The appropriate entry the from $_FILES superglobal.
+ * @param array  $file              The appropriate entry the from $_FILES superglobal.
  * @param string $upload_dir_filter A filter to be applied to 'upload_dir'.
+ *
  * @return bool True on success, false on failure.
  */
 function bp_core_avatar_handle_upload( $file, $upload_dir_filter ) {
@@ -574,98 +575,35 @@ function bp_core_avatar_handle_upload( $file, $upload_dir_filter ) {
 	 * You may want to hook into this filter if you want to override this function.
 	 * Make sure you return false.
 	 */
-	if ( !apply_filters( 'bp_core_pre_avatar_handle_upload', true, $file, $upload_dir_filter ) )
+	if ( ! apply_filters( 'bp_core_pre_avatar_handle_upload', true, $file, $upload_dir_filter ) ) {
 		return true;
-
-	require_once( ABSPATH . '/wp-admin/includes/file.php' );
-
-	$uploadErrors = array(
-		0 => __( 'The image was uploaded successfully', 'buddypress' ),
-		1 => __( 'The image exceeds the maximum allowed file size of: ', 'buddypress' ) . size_format( bp_core_avatar_original_max_filesize() ),
-		2 => __( 'The image exceeds the maximum allowed file size of: ', 'buddypress' ) . size_format( bp_core_avatar_original_max_filesize() ),
-		3 => __( 'The uploaded file was only partially uploaded.', 'buddypress' ),
-		4 => __( 'The image was not uploaded.', 'buddypress' ),
-		6 => __( 'Missing a temporary folder.', 'buddypress' )
-	);
-
-	if ( ! bp_core_check_avatar_upload( $file ) ) {
-		bp_core_add_message( sprintf( __( 'Your upload failed. Please try again. Error was: %s', 'buddypress' ), $uploadErrors[$file['file']['error']] ), 'error' );
-		return false;
 	}
 
-	if ( ! bp_core_check_avatar_size( $file ) ) {
-		bp_core_add_message( sprintf( __( 'The file you uploaded is too big. Please upload a file under %s', 'buddypress' ), size_format( bp_core_avatar_original_max_filesize() ) ), 'error' );
-		return false;
-	}
+	// Setup some variables
+	$bp          = buddypress();
+	$upload_path = bp_core_avatar_upload_path();
 
-	if ( ! bp_core_check_avatar_type( $file ) ) {
-		bp_core_add_message( __( 'Please upload only JPG, GIF or PNG photos.', 'buddypress' ), 'error' );
-		return false;
-	}
-
-	// Filter the upload location
-	add_filter( 'upload_dir', $upload_dir_filter, 10, 0 );
-
-	$bp = buddypress();
+	// Upload the file
+	$avatar_attachment = new BP_Attachment_Avatar();
+	$bp->avatar_admin->original = $avatar_attachment->upload( $file, $upload_dir_filter );
 
-	$bp->avatar_admin->original = wp_handle_upload( $file['file'], array( 'action'=> 'bp_avatar_upload' ) );
-
-	// Remove the upload_dir filter, so that other upload URLs on the page
-	// don't break
-	remove_filter( 'upload_dir', $upload_dir_filter, 10, 0 );
-
-	// Move the file to the correct upload location.
-	if ( !empty( $bp->avatar_admin->original['error'] ) ) {
+	// In case of an error, stop the process and display a feedback to the user
+	if ( ! empty( $bp->avatar_admin->original['error'] ) ) {
 		bp_core_add_message( sprintf( __( 'Upload Failed! Error was: %s', 'buddypress' ), $bp->avatar_admin->original['error'] ), 'error' );
 		return false;
 	}
 
-	// Get image size
-	$size  = @getimagesize( $bp->avatar_admin->original['file'] );
-	$error = false;
-
-	// Check image size and shrink if too large
-	if ( $size[0] > bp_core_avatar_original_max_width() ) {
-		$editor = wp_get_image_editor( $bp->avatar_admin->original['file'] );
-
-		if ( ! is_wp_error( $editor ) ) {
-			$editor->set_quality( 100 );
-
-			$resized = $editor->resize( bp_core_avatar_original_max_width(), bp_core_avatar_original_max_width(), false );
-			if ( ! is_wp_error( $resized ) ) {
-				$thumb = $editor->save( $editor->generate_filename() );
-			} else {
-				$error = $resized;
-			}
-
-			// Check for thumbnail creation errors
-			if ( false === $error && is_wp_error( $thumb ) ) {
-				$error = $thumb;
-			}
-
-			// Thumbnail is good so proceed
-			if ( false === $error ) {
-				$bp->avatar_admin->resized = $thumb;
-			}
-
-		} else {
-			$error = $editor;
-		}
-
-		if ( false !== $error ) {
-			bp_core_add_message( sprintf( __( 'Upload Failed! Error was: %s', 'buddypress' ), $error->get_error_message() ), 'error' );
-			return false;
-		}
-	}
-
-	if ( ! isset( $bp->avatar_admin->image ) )
-		$bp->avatar_admin->image = new stdClass();
+	// Maybe resize
+	$bp->avatar_admin->resized = $avatar_attachment->shrink( $bp->avatar_admin->original['file'] );
+	$bp->avatar_admin->image   = new stdClass();
 
 	// We only want to handle one image after resize.
 	if ( empty( $bp->avatar_admin->resized ) ) {
-		$bp->avatar_admin->image->dir = str_replace( bp_core_avatar_upload_path(), '', $bp->avatar_admin->original['file'] );
+		$bp->avatar_admin->image->file = $bp->avatar_admin->original['file'];
+		$bp->avatar_admin->image->dir  = str_replace( $upload_path, '', $bp->avatar_admin->original['file'] );
 	} else {
-		$bp->avatar_admin->image->dir = str_replace( bp_core_avatar_upload_path(), '', $bp->avatar_admin->resized['path'] );
+		$bp->avatar_admin->image->file = $bp->avatar_admin->resized['path'];
+		$bp->avatar_admin->image->dir  = str_replace( $upload_path, '', $bp->avatar_admin->resized['path'] );
 		@unlink( $bp->avatar_admin->original['file'] );
 	}
 
@@ -675,13 +613,9 @@ function bp_core_avatar_handle_upload( $file, $upload_dir_filter ) {
 		return false;
 	}
 
-	// If the uploaded image is smaller than the "full" dimensions, throw
-	// a warning
-	$uploaded_image = @getimagesize( bp_core_avatar_upload_path() . buddypress()->avatar_admin->image->dir );
-	$full_width     = bp_core_avatar_full_width();
-	$full_height    = bp_core_avatar_full_height();
-	if ( isset( $uploaded_image[0] ) && $uploaded_image[0] < $full_width || $uploaded_image[1] < $full_height ) {
-		bp_core_add_message( sprintf( __( 'You have selected an image that is smaller than recommended. For best results, upload a picture larger than %d x %d pixels.', 'buddypress' ), $full_width, $full_height ), 'error' );
+	// If the uploaded image is smaller than the "full" dimensions, throw a warning
+	if ( $avatar_attachment->is_too_small( $bp->avatar_admin->image->file ) ) {
+		bp_core_add_message( sprintf( __( 'You have selected an image that is smaller than recommended. For best results, upload a picture larger than %d x %d pixels.', 'buddypress' ), bp_core_avatar_full_width(), bp_core_avatar_full_height() ), 'error' );
 	}
 
 	// Set the url value for the image
@@ -738,81 +672,15 @@ function bp_core_avatar_handle_crop( $args = '' ) {
 	 * You may want to hook into this filter if you want to override this function.
 	 * Make sure you return false.
 	 */
-	if ( !apply_filters( 'bp_core_pre_avatar_handle_crop', true, $r ) )
+	if ( ! apply_filters( 'bp_core_pre_avatar_handle_crop', true, $r ) ) {
 		return true;
-
-	extract( $r, EXTR_SKIP );
-
-	if ( empty( $original_file ) )
-		return false;
-
-	$original_file = bp_core_avatar_upload_path() . $original_file;
-
-	if ( !file_exists( $original_file ) )
-		return false;
-
-	if ( empty( $item_id ) ) {
-		$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', dirname( $original_file ), $item_id, $object, $avatar_dir );
-	} else {
-		$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', bp_core_avatar_upload_path() . '/' . $avatar_dir . '/' . $item_id, $item_id, $object, $avatar_dir );
-	}
-
-	if ( !file_exists( $avatar_folder_dir ) )
-		return false;
-
-	require_once( ABSPATH . '/wp-admin/includes/image.php' );
-	require_once( ABSPATH . '/wp-admin/includes/file.php' );
-
-	// Delete the existing avatar files for the object
-	$existing_avatar = bp_core_fetch_avatar( array(
-		'object'  => $object,
-		'item_id' => $item_id,
-		'html' => false,
-	) );
-
-	if ( ! empty( $existing_avatar ) ) {
-		// Check that the new avatar doesn't have the same name as the
-		// old one before deleting
-		$upload_dir           = wp_upload_dir();
-		$existing_avatar_path = str_replace( $upload_dir['baseurl'], '', $existing_avatar );
-		$new_avatar_path      = str_replace( $upload_dir['basedir'], '', $original_file );
-
-		if ( $existing_avatar_path !== $new_avatar_path ) {
-			bp_core_delete_existing_avatar( array( 'object' => $object, 'item_id' => $item_id, 'avatar_path' => $avatar_folder_dir ) );
-		}
 	}
 
+	// Crop the file
+	$avatar_attachment = new BP_Attachment_Avatar();
+	$cropped           = $avatar_attachment->crop( $r );
 
-
-	// Make sure we at least have a width and height for cropping
-	if ( empty( $crop_w ) ) {
-		$crop_w = bp_core_avatar_full_width();
-	}
-
-	if ( empty( $crop_h ) ) {
-		$crop_h = bp_core_avatar_full_height();
-	}
-
-	// Get the file extension
-	$data = @getimagesize( $original_file );
-	$ext  = $data['mime'] == 'image/png' ? 'png' : 'jpg';
-
-	// Set the full and thumb filenames
-	$full_filename  = wp_hash( $original_file . time() ) . '-bpfull.'  . $ext;
-	$thumb_filename = wp_hash( $original_file . time() ) . '-bpthumb.' . $ext;
-
-	// Crop the image
-	$full_cropped  = wp_crop_image( $original_file, (int) $crop_x, (int) $crop_y, (int) $crop_w, (int) $crop_h, bp_core_avatar_full_width(),  bp_core_avatar_full_height(),  false, $avatar_folder_dir . '/' . $full_filename  );
-	$thumb_cropped = wp_crop_image( $original_file, (int) $crop_x, (int) $crop_y, (int) $crop_w, (int) $crop_h, bp_core_avatar_thumb_width(), bp_core_avatar_thumb_height(), false, $avatar_folder_dir . '/' . $thumb_filename );
-
-	// Check for errors
-	if ( empty( $full_cropped ) || empty( $thumb_cropped ) || is_wp_error( $full_cropped ) || is_wp_error( $thumb_cropped ) )
-		return false;
-
-	// Remove the original
-	@unlink( $original_file );
-
-	return true;
+	return is_array( $cropped );
 }
 
 /**
@@ -926,16 +794,8 @@ function bp_core_check_avatar_type($file) {
 /**
  * Fetch data from the BP root blog's upload directory.
  *
- * Handy for multisite instances because all uploads are made on the BP root
- * blog and we need to query the BP root blog for the upload directory data.
- *
- * This function ensures that we only need to use {@link switch_to_blog()}
- * once to get what we need.
- *
  * @since BuddyPress (1.8.0)
  *
- * @uses wp_upload_dir()
- *
  * @param string $type The variable we want to return from the $bp->avatars
  *        object. Only 'upload_path' and 'url' are supported. Default: 'upload_path'.
  * @return string The avatar upload directory path.
@@ -977,16 +837,9 @@ function bp_core_get_upload_dir( $type = 'upload_path' ) {
 
 			// No cache, so query for it
 			} else {
-				// We need to switch to the root blog on multisite installs
-				if ( is_multisite() ) {
-					switch_to_blog( bp_get_root_blog_id() );
-				}
 
 				// Get upload directory information from current site
-				$upload_dir = wp_upload_dir();
-
-				// Will bail if not switched
-				restore_current_blog();
+				$upload_dir = bp_upload_dir();
 
 				// Stash upload directory data for later use
 				$bp->avatar->upload_dir = $upload_dir;
@@ -1018,7 +871,7 @@ function bp_core_get_upload_dir( $type = 'upload_path' ) {
 /**
  * Get the absolute upload path for the WP installation.
  *
- * @uses wp_upload_dir To get upload directory info
+ * @uses bp_core_get_upload_dir() To get upload directory info
  *
  * @return string Absolute path to WP upload directory.
  */
@@ -1029,7 +882,7 @@ function bp_core_avatar_upload_path() {
 /**
  * Get the raw base URL for root site upload location.
  *
- * @uses wp_upload_dir To get upload directory info.
+ * @uses bp_core_get_upload_dir() To get upload directory info.
  *
  * @return string Full URL to current upload location.
  */
diff --git src/bp-core/bp-core-classes.php src/bp-core/bp-core-classes.php
index b50325d..d23e878 100644
--- src/bp-core/bp-core-classes.php
+++ src/bp-core/bp-core-classes.php
@@ -20,3 +20,5 @@ require dirname( __FILE__ ) . '/classes/class-bp-walker-nav-menu-checklist.php';
 require dirname( __FILE__ ) . '/classes/class-bp-suggestions.php';
 require dirname( __FILE__ ) . '/classes/class-bp-members-suggestions.php';
 require dirname( __FILE__ ) . '/classes/class-bp-recursive-query.php';
+require dirname( __FILE__ ) . '/classes/class-bp-attachment.php';
+require dirname( __FILE__ ) . '/classes/class-bp-attachment-avatar.php';
diff --git src/bp-core/bp-core-functions.php src/bp-core/bp-core-functions.php
index 0671b49..5b7bead 100644
--- src/bp-core/bp-core-functions.php
+++ src/bp-core/bp-core-functions.php
@@ -2137,3 +2137,50 @@ function bp_core_get_suggestions( $args ) {
 
 	return apply_filters( 'bp_core_get_suggestions', $retval, $args );
 }
+
+/**
+ * Set data from the BP root blog's upload directory.
+ *
+ * Handy for multisite instances because all uploads are made on the BP root
+ * blog and we need to query the BP root blog for the upload directory data.
+ *
+ * This function ensures that we only need to use {@link switch_to_blog()}
+ * once to get what we need.
+ *
+ * @since BuddyPress (2.3.0)
+ *
+ * @uses  is_multisite()
+ * @uses  bp_is_root_blog()
+ * @uses  switch_to_blog()
+ * @uses  wp_upload_dir()
+ * @uses  restore_current_blog()
+ */
+function bp_upload_dir() {
+	$bp = buddypress();
+
+	if ( empty( $bp->upload_dir ) ) {
+		$need_switch = (bool) ( is_multisite() && ! bp_is_root_blog() );
+
+		// Maybe juggle to root blog
+		if ( true === $need_switch ) {
+			switch_to_blog( bp_get_root_blog_id() );
+		}
+
+		// Get the upload directory (maybe for root blog)
+		$wp_upload_dir = wp_upload_dir();
+
+		// Maybe juggle back to current blog
+		if ( true === $need_switch ) {
+			restore_current_blog();
+		}
+
+		// Bail if an error occurred
+		if ( ! empty( $wp_upload_dir['error'] ) ) {
+			return false;
+		}
+
+		$bp->upload_dir = $wp_upload_dir;
+	}
+
+	return $bp->upload_dir;
+}
diff --git src/bp-core/classes/class-bp-attachment-avatar.php src/bp-core/classes/class-bp-attachment-avatar.php
index e69de29..3df7924 100644
--- src/bp-core/classes/class-bp-attachment-avatar.php
+++ src/bp-core/classes/class-bp-attachment-avatar.php
@@ -0,0 +1,269 @@
+<?php
+
+/**
+ * Core avatar attachment class
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BP Attachment class to manage your avatar upload needs
+ *
+ * @since BuddyPress (2.3.0)
+ */
+class BP_Attachment_Avatar extends BP_Attachment {
+
+	/**
+	 * Construct Upload parameters
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @see  BP_Attachment::__construct() for list of parameters
+	 * @uses sanitize_key()
+	 * @uses bp_parse_args()
+	 * @uses BP_Attachment->set_upload_error_strings()
+	 * @uses BP_Attachment->set_upload_dir()
+	 */
+	public function __construct() {
+		parent::__construct( array(
+			// Upload action
+			'action' => 'bp_avatar_upload',
+
+			// Specific errors for avatars
+			'upload_error_strings'  => array(
+				sprintf( __( 'That photo is too big. Please upload one smaller than %s', 'buddypress' ), size_format( bp_core_avatar_original_max_filesize() ) ),
+				__( 'Please upload only JPG, GIF or PNG photos.', 'buddypress' ),
+			),
+		) );
+	}
+
+	/**
+	 * Set Upload Dir data for avatars
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @uses bp_core_avatar_upload_path()
+	 * @uses bp_core_avatar_url()
+	 * @uses bp_upload_dir()
+	 */
+	public function set_upload_dir() {
+		if ( bp_core_avatar_upload_path() && bp_core_avatar_url() ) {
+			$this->upload_path = bp_core_avatar_upload_path();
+			$this->url         = bp_core_avatar_url();
+			$this->upload_dir  = bp_upload_dir();
+		} else {
+			parent::set_upload_dir();
+		}
+	}
+
+	/**
+	 * Avatar specific rules
+	 *
+	 * Adds an error if the avatar size or type don't match BuddyPress needs
+	 * The error code is the index of $upload_error_strings
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param  array $file the temporary file attributes (before it has been moved)
+	 * @uses   bp_core_check_avatar_size()
+	 * @uses   bp_core_check_avatar_type()
+	 * @return array the file with extra errors if needed
+	 */
+	public function validate_upload( $file = array() ) {
+
+		// Bail if already an error
+		if ( ! empty( $file['error'] ) ) {
+			return $file;
+		}
+
+		// File size is too big
+		if ( ! bp_core_check_avatar_size( array( 'file' => $file ) ) ) {
+			$file['error'] = 9;
+
+		// File is of invalid type
+		} else if ( ! bp_core_check_avatar_type( array( 'file' => $file ) ) ) {
+			$file['error'] = 10;
+		}
+
+		// Return with error code attached
+		return $file;
+	}
+
+	/**
+	 * Maybe shrink the attachment to fit maximum allowed width
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @return mixed
+	 */
+	public static function shrink( $file = array() ) {
+
+		// Get image size
+		$size   = @getimagesize( $file );
+		$retval = false;
+
+		// Check image size and shrink if too large
+		if ( $size[0] > bp_core_avatar_original_max_width() ) {
+			$editor = wp_get_image_editor( $file );
+
+			if ( ! is_wp_error( $editor ) ) {
+				$editor->set_quality( 100 );
+
+				$resized = $editor->resize( bp_core_avatar_original_max_width(), bp_core_avatar_original_max_width(), false );
+				if ( ! is_wp_error( $resized ) ) {
+					$thumb = $editor->save( $editor->generate_filename() );
+				} else {
+					$retval = $resized;
+				}
+
+				// Check for thumbnail creation errors
+				if ( ( false === $retval ) && is_wp_error( $thumb ) ) {
+					$retval = $thumb;
+				}
+
+				// Thumbnail is good so proceed
+				if ( false === $retval ) {
+					$retval = $thumb;
+				}
+
+			} else {
+				$retval = $editor;
+			}
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Check if the image dimensions are smaller than full avatar dimensions
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param type $file
+	 * @return boolean
+	 */
+	public static function is_too_small( $file = array() ) {
+		$uploaded_image = @getimagesize( $file );
+		$full_width     = bp_core_avatar_full_width();
+		$full_height    = bp_core_avatar_full_height();
+
+		if ( isset( $uploaded_image[0] ) && $uploaded_image[0] < $full_width || $uploaded_image[1] < $full_height ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Crop the avatar
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @see  bp_core_avatar_handle_crop() for list of parameters
+	 * @param array $args
+	 * @return array the cropped avatars (full and thumb)
+	 */
+	public function crop( $args = array() ) {
+		// Bail if the original file is missing
+		if ( empty( $args['original_file'] ) ) {
+			return false;
+		}
+
+		// We need to manipulate the image
+		$this->required_wp_files[] = 'image';
+
+		/**
+		 * Original file is a relative path to the image
+		 * eg: /avatars/1/avatar.jpg
+		 */
+		$relative_path = $args['original_file'];
+		$absolute_path = $this->upload_path . $relative_path;
+
+		// Bail if the avatar is not available
+		if ( ! file_exists( $absolute_path ) )  {
+			return false;
+		}
+
+		if ( empty( $args['item_id'] ) ) {
+			$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', dirname( $absolute_path ), $args['item_id'], $args['object'], $args['avatar_dir'] );
+		} else {
+			$avatar_folder_dir = apply_filters( 'bp_core_avatar_folder_dir', $this->upload_path . '/' . $args['avatar_dir'] . '/' . $args['item_id'], $args['item_id'], $args['object'], $args['avatar_dir'] );
+		}
+
+		// Bail if the avatar folder is missing for this item_id
+		if ( ! file_exists( $avatar_folder_dir ) ) {
+			return false;
+		}
+
+		// Include the WordPress core needed files
+		$this->includes();
+
+		// Delete the existing avatar files for the object
+		$existing_avatar = bp_core_fetch_avatar( array(
+			'object'  => $args['object'],
+			'item_id' => $args['item_id'],
+			'html' => false,
+		) );
+
+		/**
+		 * Check that the new avatar doesn't have the same name as the
+		 * old one before deleting
+		 */
+		if ( ! empty( $existing_avatar ) && $existing_avatar !== $this->url . $relative_path ) {
+			bp_core_delete_existing_avatar( array( 'object' => $args['object'], 'item_id' => $args['item_id'], 'avatar_path' => $avatar_folder_dir ) );
+		}
+
+		// Make sure we at least have minimal data for cropping
+		if ( empty( $args['crop_w'] ) ) {
+			$crop_w = bp_core_avatar_full_width();
+		} else {
+			$crop_w = $args['crop_w'];
+		}
+
+		if ( empty( $args['crop_h'] ) ) {
+			$crop_h = bp_core_avatar_full_height();
+		} else {
+			$crop_h = $args['crop_h'];
+		}
+
+		$crop_x = 0;
+		if ( ! empty( $args['crop_x'] ) ) {
+			$crop_x = $args['crop_x'];
+		}
+
+		$crop_y = 0;
+		if ( ! empty( $args['crop_y'] ) ) {
+			$crop_y = $args['crop_y'];
+		}
+
+		// Get the file extension
+		$data = @getimagesize( $absolute_path );
+		$ext  = $data['mime'] == 'image/png' ? 'png' : 'jpg';
+
+		// Set the full and thumb filenames
+		$full_filename  = wp_hash( $absolute_path . time() ) . '-bpfull.'  . $ext;
+		$thumb_filename = wp_hash( $absolute_path . time() ) . '-bpthumb.' . $ext;
+
+		// Crop the image
+		$full_cropped  = wp_crop_image( $absolute_path, (int) $crop_x, (int) $crop_y, (int) $crop_w, (int) $crop_h, bp_core_avatar_full_width(),  bp_core_avatar_full_height(),  false, $avatar_folder_dir . '/' . $full_filename  );
+		$thumb_cropped = wp_crop_image( $absolute_path, (int) $crop_x, (int) $crop_y, (int) $crop_w, (int) $crop_h, bp_core_avatar_thumb_width(), bp_core_avatar_thumb_height(), false, $avatar_folder_dir . '/' . $thumb_filename );
+
+		// Check for errors
+		if ( empty( $full_cropped ) || empty( $thumb_cropped ) || is_wp_error( $full_cropped ) || is_wp_error( $thumb_cropped ) ) {
+			return false;
+		}
+
+		// Remove the original
+		@unlink( $absolute_path );
+
+		// Return the full and thumb cropped avatars
+		return array(
+			'full'  => trailingslashit( $avatar_folder_dir ) . $full_filename,
+			'thumb' => trailingslashit( $avatar_folder_dir ) . $thumb_filename,
+		);
+	}
+}
diff --git src/bp-core/classes/class-bp-attachment.php src/bp-core/classes/class-bp-attachment.php
index e69de29..21708f1 100644
--- src/bp-core/classes/class-bp-attachment.php
+++ src/bp-core/classes/class-bp-attachment.php
@@ -0,0 +1,372 @@
+<?php
+
+/**
+ * Core attachment class.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * BP Attachment class to manage your component's uploads
+ *
+ * @since BuddyPress (2.3.0)
+ */
+class BP_Attachment {
+
+	/** Upload properties *****************************************************/
+
+	/**
+	 * The file being uploaded
+	 *
+	 * @var array
+	 */
+	public $attachment = array();
+
+	/**
+	 * Maximum file size in kilobytes
+	 *
+	 * @var int
+	 */
+	public $original_max_filesize = 5120000;
+
+	/**
+	 * List of allowed file extensions
+	 * Defaults to get_allowed_mime_types()
+	 *
+	 * @var int
+	 */
+	public $allowed_mime_types = array();
+
+	/**
+	 * component's upload base directory.
+	 *
+	 * @var string
+	 */
+	public $base_dir = '';
+
+	/**
+	 * The upload action.
+	 *
+	 * @var string
+	 */
+	public $action = '';
+
+	/**
+	 * The file input name attribute
+	 *
+	 * @var string
+	 */
+	public $file_input = 'file';
+
+	/**
+	 * List of upload errors.
+	 *
+	 * @var array
+	 */
+	public $upload_error_strings = array();
+
+	/**
+	 * List of required core files
+	 *
+	 * @var array
+	 */
+	public $required_wp_files = array( 'file' );
+
+	/**
+	 * Construct Upload parameters
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param array $args {
+	 *     @type int    $original_max_filesize Maximum file size in kilobytes. Default: 5120000.
+	 *     @type array  $allowed_mime_types    List of allowed file extensions (eg: array( 'jpg', 'gif', 'png' ) ).
+	 *                                         Defaults to WordPress allowed mime types
+	 *     @type string $base_dir              Component's upload base directory. Defaults to WordPress 'uploads'
+	 *     @type string $action                The upload action used when uploading a file, $_POST['action'] must be set
+	 *                                         and its value must equal $action {@link wp_handle_upload()}
+	 *     @type string $file_input            The name attribute used in the file input. Default: file
+	 *     @type array  $upload_error_strings  A list of specific error messages (optional).
+	 *     @type array  $required_wp_files     The list of required WordPress core files. Default: array( 'file' );
+	 * }
+	 * @uses  sanitize_key()
+	 * @uses  bp_parse_args()
+	 * @uses  BP_Attachment->set_upload_error_strings()
+	 * @uses  BP_Attachment->set_upload_dir()
+	 */
+	public function __construct( $args = '' ) {
+
+		/**
+		 * Sanitize the action ID
+		 */
+		if ( ! empty( $args['action'] ) ) {
+			$this->action = sanitize_key( $args['action'] );
+		}
+
+		$params = bp_parse_args( $args, get_class_vars( __CLASS__ ), $this->action . '_upload_params' );
+
+		foreach ( $params as $key => $param ) {
+			if ( 'upload_error_strings' === $key ) {
+				$this->{$key} = $this->set_upload_error_strings( $param );
+			} else {
+				$this->{$key} = $param;
+			}
+		}
+
+		// Set the path/url and base dir for uploads
+		$this->set_upload_dir();
+	}
+
+	/**
+	 * Set upload path and url for the component.
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @uses  bp_upload_dir()
+	 */
+	public function set_upload_dir() {
+		// Set the directory, path, & url variables
+		$this->upload_dir  = bp_upload_dir();
+
+		if ( empty( $this->upload_dir ) ) {
+			return false;
+		}
+
+		$this->upload_path = $this->upload_dir['basedir'];
+		$this->url         = $this->upload_dir['baseurl'];
+
+		// Ensure URL is https if SSL is set/forced
+		if ( is_ssl() ) {
+			$this->url = str_replace( 'http://', 'https://', $this->url );
+		}
+
+		/**
+		 * Custom base dir.
+		 *
+		 * If the component set this property, set the specific path, url and create the dir
+		 */
+		if ( ! empty( $this->base_dir ) ) {
+			$this->upload_path = trailingslashit( $this->upload_path ) . $this->base_dir;
+			$this->url         = trailingslashit( $this->url  ) . $this->base_dir;
+
+			// Finally create the base dir
+			$this->create_dir();
+		}
+	}
+
+	/**
+	 * Set Upload error messages
+	 *
+	 * Used into the $overrides argument of BP_Attachment->upload()
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param array $param a list of error messages to add to BuddyPress core ones
+	 */
+	public function set_upload_error_strings( $param = array() ) {
+		/**
+		 * Index of the array is the error code
+		 * Custom errors will start at 9 code
+		 */
+		return array_merge( array(
+			0 => __( 'The file was uploaded successfully', 'buddypress' ),
+			1 => __( 'The uploaded file exceeds the maximum allowed file size on this site', 'buddypress' ),
+			2 => sprintf( __( 'The uploaded file exceeds the maximum allowed file size of: %s', 'buddypress' ), size_format( $this->original_max_filesize ) ),
+			3 => __( 'The uploaded file was only partially uploaded.', 'buddypress' ),
+			4 => __( 'No file was uploaded.', 'buddypress' ),
+			5 => '',
+			6 => __( 'Missing a temporary folder.', 'buddypress' ),
+			7 => __( 'Failed to write file to disk.', 'buddypress' ),
+			8 => __( 'File upload stopped by extension.', 'buddypress' ),
+		), (array) $param );
+	}
+
+	/**
+	 * Include the WordPress core needed files
+	 *
+	 * @since BuddyPress (2.3.0)
+	 */
+	public function includes() {
+		foreach ( $this->required_wp_files as $wp_file ) {
+			if ( ! file_exists( ABSPATH . "/wp-admin/includes/{$wp_file}.php" ) ) {
+				continue;
+			}
+
+			require_once( ABSPATH . "/wp-admin/includes/{$wp_file}.php" );
+		}
+	}
+
+	/**
+	 * Upload the attachment
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param  array $file               The appropriate entry the from $_FILES superglobal.
+	 * @param  string $upload_dir_filter A specific filter to be applied to 'upload_dir' (optional).
+	 * @uses   wp_handle_upload()
+	 * @uses   apply_filters()           Call 'bp_attachment_upload_overrides' to include specific upload overrides
+	 *
+	 * @return array                     On success, returns an associative array of file attributes.
+	 *                                   On failure, returns an array containing the error message
+	 *                                   (eg: array( 'error' => $message ) )
+	 */
+	public function upload( $file, $upload_dir_filter = '' ) {
+		if ( ! empty( $this->action ) ) {
+			/**
+			 * Add custom rules before enabling the file upload
+			 */
+			add_filter( "{$this->action}_prefilter", array( $this, 'validate_upload' ), 10, 1 );
+		}
+
+		// Set Default overrides
+		$overrides = array(
+			'action'               => $this->action,
+			'upload_error_strings' => $this->upload_error_strings,
+		);
+
+		/**
+		 * Add a mime override if needed
+		 * Used to restrict uploads by extensions
+		 */
+		if ( ! empty( $this->allowed_mime_types ) ) {
+			$mime_types = $this->validate_mime_types();
+
+			if ( ! empty( $mime_types ) ) {
+				$overrides['mimes'] = $mime_types;
+			}
+		}
+
+		/**
+		 * If you need to add some overrides we haven't thought of
+		 *
+		 * @var  array $overrides the wp_handle_upload overrides
+		 */
+		$overrides = apply_filters( 'bp_attachment_upload_overrides', $overrides );
+
+		$this->includes();
+
+		/**
+		 * If the $base_dir was set when constructing the class,
+		 * and no specific filter has been requested, use a default
+		 * filter to create the specific $base dir
+		 * @see  BP_Attachment->upload_dir_filter()
+		 */
+		if ( empty( $upload_dir_filter ) && ! empty( $this->base_dir ) ) {
+			$upload_dir_filter = array( $this, 'upload_dir_filter' );
+		}
+
+		// Make sure the file will be uploaded in the attachment directory
+		add_filter( 'upload_dir', $upload_dir_filter, 10, 0 );
+
+		// Upload the attachment
+		$this->attachment = wp_handle_upload( $file[ $this->file_input ], $overrides );
+
+		// Restore WordPress Uploads data
+		remove_filter( 'upload_dir', $upload_dir_filter, 10, 0 );
+
+		// Finally return the uploaded file or the error
+		return $this->attachment;
+	}
+
+	/**
+	 * Validate the allowed mime types using WordPress allowed mime types
+	 *
+	 * In case of a multisite, the mime types are already restricted by
+	 * the 'upload_filetypes' setting. BuddyPress will respect this setting.
+	 * @see check_upload_mimes()
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @uses get_allowed_mime_types()
+	 */
+	protected function validate_mime_types() {
+		$wp_mimes = get_allowed_mime_types();
+		$valid_mimes = array();
+
+		// Set the allowed mimes for the upload
+		foreach ( (array) $this->allowed_mime_types as $ext ) {
+			foreach ( $wp_mimes as $ext_pattern => $mime ) {
+				if ( $ext != '' && strpos( $ext_pattern, $ext ) !== false ) {
+					$valid_mimes[$ext_pattern] = $mime;
+				}
+			}
+		}
+		return $valid_mimes;
+	}
+
+	/**
+	 * Specific upload rules
+	 *
+	 * Override this function from your child class to build your specific rules
+	 * @see BP_Attachment_Avatar->validate_upload() for an example of use
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param  array $file the temporary file attributes (before it has been moved)
+	 * @return array the file
+	 */
+	public function validate_upload( $file = array() ) {
+		// Simply return the file, rules must be set in child class
+		return $file;
+	}
+
+	/**
+	 * Default filter to save the attachments
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @uses   apply_filters() call 'bp_attachment_upload_dir' to eventually override the upload location
+	 *                         regarding to context
+	 * @return array the upload directory data
+	 */
+	public function upload_dir_filter() {
+
+		/**
+		 * Filters the component's upload directory.
+		 *
+		 * @since BuddyPress (2.3.0)
+		 *
+		 * @param array $value Array containing the path, URL, and other helpful settings.
+		 */
+		return apply_filters( 'bp_attachment_upload_dir', array(
+			'path'    => $this->upload_path,
+			'url'     => $this->url,
+			'subdir'  => false,
+			'basedir' => $this->upload_path,
+			'baseurl' => $this->url,
+			'error'   => false
+		) );
+	}
+
+	/**
+	 * Create the custom base directory for the component uploads
+	 *
+	 * Override this function in your child class to run specific actions
+	 * (eg: add an .htaccess file)
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @uses   wp_mkdir_p()
+	 */
+	public function create_dir() {
+		// Bail if no specific base dir is set
+		if ( empty( $this->base_dir ) ) {
+			return false;
+		}
+
+		// Check if upload path already exists
+		if ( ! file_exists( $this->upload_path ) ) {
+
+			// If path does not exist, attempt to create it
+			if ( ! wp_mkdir_p( $this->upload_path ) ) {
+				return false;
+			}
+		}
+
+		// Directory exists
+		return true;
+	}
+}
diff --git src/bp-groups/bp-groups-functions.php src/bp-groups/bp-groups-functions.php
index 011231f..49864eb 100644
--- src/bp-groups/bp-groups-functions.php
+++ src/bp-groups/bp-groups-functions.php
@@ -817,15 +817,12 @@ function groups_avatar_upload_dir( $group_id = 0 ) {
 		$group_id = bp_get_current_group_id();
 	}
 
-	$path    = bp_core_avatar_upload_path() . '/group-avatars/' . $group_id;
-	$newbdir = $path;
-
-	if ( !file_exists( $path ) )
-		@wp_mkdir_p( $path );
-
-	$newurl    = bp_core_avatar_url() . '/group-avatars/' . $group_id;
+	$directory = 'group-avatars';
+	$path      = bp_core_avatar_upload_path() . '/' . $directory . '/' . $group_id;
+	$newbdir   = $path;
+	$newurl    = bp_core_avatar_url() . '/' . $directory . '/' . $group_id;
 	$newburl   = $newurl;
-	$newsubdir = '/group-avatars/' . $group_id;
+	$newsubdir = '/' . $directory . '/' . $group_id;
 
 	/**
 	 * Filters the avatar upload directory path for a given group.
@@ -834,7 +831,14 @@ function groups_avatar_upload_dir( $group_id = 0 ) {
 	 *
 	 * @param array $value Array of parts related to the groups avatar upload directory.
 	 */
-	return apply_filters( 'groups_avatar_upload_dir', array( 'path' => $path, 'url' => $newurl, 'subdir' => $newsubdir, 'basedir' => $newbdir, 'baseurl' => $newburl, 'error' => false ) );
+	return apply_filters( 'groups_avatar_upload_dir', array(
+		'path'    => $path,
+		'url'     => $newurl,
+		'subdir'  => $newsubdir,
+		'basedir' => $newbdir,
+		'baseurl' => $newburl,
+		'error'   => false
+	) );
 }
 
 /** Group Member Status Checks ************************************************/
diff --git src/bp-members/bp-members-functions.php src/bp-members/bp-members-functions.php
index 346e7f9..4bf7a99 100644
--- src/bp-members/bp-members-functions.php
+++ src/bp-members/bp-members-functions.php
@@ -2159,16 +2159,12 @@ function bp_core_signup_avatar_upload_dir() {
 		return false;
 	}
 
-	$path  = bp_core_avatar_upload_path() . '/avatars/signups/' . $bp->signup->avatar_dir;
-	$newbdir = $path;
-
-	if ( ! file_exists( $path ) ) {
-		@wp_mkdir_p( $path );
-	}
-
-	$newurl = bp_core_avatar_url() . '/avatars/signups/' . $bp->signup->avatar_dir;
-	$newburl = $newurl;
-	$newsubdir = '/avatars/signups/' . $bp->signup->avatar_dir;
+	$directory = 'avatars/signups';
+	$path      = bp_core_avatar_upload_path() . '/' . $directory . '/' . $bp->signup->avatar_dir;
+	$newbdir   = $path;
+	$newurl    = bp_core_avatar_url() . '/' . $directory . '/' . $bp->signup->avatar_dir;
+	$newburl   = $newurl;
+	$newsubdir = '/' . $directory . '/' . $bp->signup->avatar_dir;
 
 	/**
 	 * Filters the avatar storage directory for use during registration.
@@ -2183,7 +2179,7 @@ function bp_core_signup_avatar_upload_dir() {
 		'subdir'  => $newsubdir,
 		'basedir' => $newbdir,
 		'baseurl' => $newburl,
-		'error' => false
+		'error'   => false
 	) );
 }
 
diff --git src/bp-xprofile/bp-xprofile-functions.php src/bp-xprofile/bp-xprofile-functions.php
index 16c1599..62c37ed 100644
--- src/bp-xprofile/bp-xprofile-functions.php
+++ src/bp-xprofile/bp-xprofile-functions.php
@@ -680,13 +680,8 @@ function xprofile_avatar_upload_dir( $directory = 'avatars', $user_id = 0 ) {
 		$directory = 'avatars';
 	}
 
-	$path    = bp_core_avatar_upload_path() . '/' . $directory. '/' . $user_id;
-	$newbdir = $path;
-
-	if ( ! file_exists( $path ) ) {
-		@wp_mkdir_p( $path );
-	}
-
+	$path      = bp_core_avatar_upload_path() . '/' . $directory. '/' . $user_id;
+	$newbdir   = $path;
 	$newurl    = bp_core_avatar_url() . '/' . $directory. '/' . $user_id;
 	$newburl   = $newurl;
 	$newsubdir = '/' . $directory. '/' . $user_id;
