diff --git src/bp-core/bp-core-avatars.php src/bp-core/bp-core-avatars.php
index c607bb2..447f913 100644
--- src/bp-core/bp-core-avatars.php
+++ src/bp-core/bp-core-avatars.php
@@ -52,39 +52,47 @@ add_action( 'bp_init', 'bp_core_set_avatar_constants', 3 );
 function bp_core_set_avatar_globals() {
 	$bp = buddypress();
 
-	$bp->avatar        = new stdClass;
-	$bp->avatar->thumb = new stdClass;
-	$bp->avatar->full  = new stdClass;
-
-	// Dimensions
-	$bp->avatar->thumb->width  = BP_AVATAR_THUMB_WIDTH;
-	$bp->avatar->thumb->height = BP_AVATAR_THUMB_HEIGHT;
-	$bp->avatar->full->width   = BP_AVATAR_FULL_WIDTH;
-	$bp->avatar->full->height  = BP_AVATAR_FULL_HEIGHT;
-
-	// Upload maximums
-	$bp->avatar->original_max_width    = BP_AVATAR_ORIGINAL_MAX_WIDTH;
-	$bp->avatar->original_max_filesize = BP_AVATAR_ORIGINAL_MAX_FILESIZE;
-
-	// Defaults
-	$bp->avatar->thumb->default = bp_core_avatar_default_thumb();
-	$bp->avatar->full->default  = bp_core_avatar_default();
-
-	// These have to be set on page load in order to avoid infinite filter loops at runtime
-	$bp->avatar->upload_path = bp_core_avatar_upload_path();
-	$bp->avatar->url = bp_core_avatar_url();
-
-	// Cache the root blog's show_avatars setting, to avoid unnecessary
-	// calls to switch_to_blog()
-	$bp->avatar->show_avatars = (bool) BP_SHOW_AVATARS;
+	$bp->avatar = new BP_Attachment_Avatar( array(
+
+		// Dimensions for thumb size
+		'thumb' => (object) array(
+			'width'   => absint( BP_AVATAR_THUMB_WIDTH ),
+			'height'  => absint( BP_AVATAR_THUMB_HEIGHT ),
+		),
+
+		// Dimensions for full size
+		'full' => (object) array(
+			'width'   => absint( BP_AVATAR_FULL_WIDTH ),
+			'height'  => absint( BP_AVATAR_FULL_HEIGHT ),
+		),
+
+		// Upload maximums
+		'original_max_width'    => absint( BP_AVATAR_ORIGINAL_MAX_WIDTH ),
+		'original_max_filesize' => absint( BP_AVATAR_ORIGINAL_MAX_FILESIZE ),
+
+		// Cache the root blog's show_avatars setting, to avoid unnecessary
+		// calls to switch_to_blog()
+		'show_avatars'          => (bool) BP_SHOW_AVATARS,
+
+		// 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_AVATAR_ORIGINAL_MAX_FILESIZE ) ),
+			__( 'Please upload only JPG, GIF or PNG photos.', 'buddypress' ),
+		),
+	) );
 
 	// Backpat for pre-1.5
-	if ( ! defined( 'BP_AVATAR_UPLOAD_PATH' ) )
+	if ( ! defined( 'BP_AVATAR_UPLOAD_PATH' ) ) {
 		define( 'BP_AVATAR_UPLOAD_PATH', $bp->avatar->upload_path );
+	}
 
 	// Backpat for pre-1.5
-	if ( ! defined( 'BP_AVATAR_URL' ) )
+	if ( ! defined( 'BP_AVATAR_URL' ) ) {
 		define( 'BP_AVATAR_URL', $bp->avatar->url );
+	}
 
 	do_action( 'bp_core_set_avatar_globals' );
 }
@@ -564,8 +572,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 +583,34 @@ 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;
 	}
 
-	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();
+	// Setup some variables
+	$bp          = buddypress();
+	$upload_path = bp_core_avatar_upload_path();
 
-	$bp->avatar_admin->original = wp_handle_upload( $file['file'], array( 'action'=> 'bp_avatar_upload' ) );
+	// Upload the file
+	$bp->avatar_admin->original = $bp->avatar->upload( $file, $upload_dir_filter );
 
-	// 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 = $bp->avatar->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 +620,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 ( $bp->avatar->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
@@ -926,16 +867,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.
@@ -943,76 +876,11 @@ function bp_core_check_avatar_type($file) {
 function bp_core_get_upload_dir( $type = 'upload_path' ) {
 	$bp = buddypress();
 
-	switch ( $type ) {
-		case 'upload_path' :
-			$constant = 'BP_AVATAR_UPLOAD_PATH';
-			$key      = 'basedir';
-
-			break;
-
-		case 'url' :
-			$constant = 'BP_AVATAR_URL';
-			$key      = 'baseurl';
-
-			break;
-
-		default :
-			return false;
-
-			break;
-	}
-
-	// See if the value has already been calculated and stashed in the $bp global
-	if ( isset( $bp->avatar->$type ) ) {
-		$retval = $bp->avatar->$type;
-	} else {
-		// If this value has been set in a constant, just use that
-		if ( defined( $constant ) ) {
-			$retval = constant( $constant );
-		} else {
-
-			// Use cached upload dir data if available
-			if ( ! empty( $bp->avatar->upload_dir ) ) {
-				$upload_dir = $bp->avatar->upload_dir;
-
-			// 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();
-
-				// Stash upload directory data for later use
-				$bp->avatar->upload_dir = $upload_dir;
-			}
-
-			// Directory does not exist and cannot be created
-			if ( ! empty( $upload_dir['error'] ) ) {
-				$retval = '';
-
-			} else {
-				$retval = $upload_dir[$key];
-
-				// If $key is 'baseurl', check to see if we're on SSL
-				// Workaround for WP13941, WP15928, WP19037.
-				if ( $key == 'baseurl' && is_ssl() ) {
-					$retval = str_replace( 'http://', 'https://', $retval );
-				}
-			}
-
-		}
-
-		// Stash in $bp for later use
-		$bp->avatar->$type = $retval;
+	if ( ! isset( $bp->avatar->{$type} ) ) {
+		return false;
 	}
 
-	return $retval;
+	return $bp->avatar->{$type};
 }
 
 /**
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/classes/class-bp-attachment-avatar.php src/bp-core/classes/class-bp-attachment-avatar.php
index e69de29..f01db21 100644
--- src/bp-core/classes/class-bp-attachment-avatar.php
+++ src/bp-core/classes/class-bp-attachment-avatar.php
@@ -0,0 +1,149 @@
+<?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( $args = '' ) {
+		parent::__construct( $args );
+
+		// Set default thumb & full values
+		if ( isset( $this->thumb, $this->full ) ) {
+			$this->thumb->default = bp_core_avatar_default_thumb();
+			$this->full->default  = bp_core_avatar_default();
+		}
+	}
+
+	/**
+	 * Eventually override default path and url to upload dir
+	 * if BP_AVATAR_UPLOAD_PATH && BP_AVATAR_URL are defined
+	 *
+	 * @since BuddyPress (2.3.0)
+	 */
+	public function set_upload_dir() {
+		parent::set_upload_dir();
+
+		// Defer to constants if set
+		if ( defined( 'BP_AVATAR_UPLOAD_PATH' ) && defined( 'BP_AVATAR_URL' ) ) {
+			$this->upload_path = BP_AVATAR_UPLOAD_PATH;
+			$this->url         = BP_AVATAR_URL;
+		}
+	}
+
+	/**
+	 * 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
+	 *
+	 * @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;
+	}
+
+	/**
+	 *
+	 * @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;
+	}
+}
diff --git src/bp-core/classes/class-bp-attachment.php src/bp-core/classes/class-bp-attachment.php
index e69de29..9030267 100644
--- src/bp-core/classes/class-bp-attachment.php
+++ src/bp-core/classes/class-bp-attachment.php
@@ -0,0 +1,399 @@
+<?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 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()
+	 */
+	public function set_upload_dir() {
+		$bp = buddypress();
+
+		if ( empty( $bp->upload_dir ) ) {
+			// Do we need to juggle which blog to get the upload dir for?
+			$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;
+			}
+
+			$bp->upload_dir = $wp_upload_dir;
+		}
+
+		// Set the directory, path, & url variables
+		$this->upload_dir  = $bp->upload_dir;
+		$this->upload_path = $bp->upload_dir['basedir'];
+		$this->url         = $bp->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 ( ! empty( $this->base_dir ) ) {
+			$this->upload_path = trailingslashit( $this->upload_path ) . $this->base_dir;
+			$this->url         = trailingslashit( $this->upload_url  ) . $this->base_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)
+	 */
+	private 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   wp_mkdir_p()
+	 * @uses   do_action() call 'bp_attachment_base_upload_dir_created' to run specific action once the
+	 *                     $base_dir is created (eg: add an .htaccess file)
+	 * @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
+		) );
+	}
+
+	public function create_dir() {
+
+		// Bail if we are allowing WordPress to make directories for us
+		if ( empty( $this->create_dir ) ) {
+			return;
+		}
+
+		// 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;
+			}
+
+			/**
+			 * Use this filter if you need to run specific actions
+			 * once the directory is created
+			 *
+			 * @since BuddyPress (2.3.0)
+			 *
+			 * @var  string $component the component id
+			 * @var  string $base_dir the specific base dir used by the component
+			 */
+			do_action( 'bp_attachment_base_upload_dir_created', $this->component, $this->base_dir );
+		}
+	}
+}
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;
diff --git tests/phpunit/testcases/core/avatars.php tests/phpunit/testcases/core/avatars.php
index d37fe1a..d11c436 100644
--- tests/phpunit/testcases/core/avatars.php
+++ tests/phpunit/testcases/core/avatars.php
@@ -35,6 +35,51 @@ class BP_Tests_Avatars extends BP_UnitTestCase {
 	}
 
 	/**
+	 * @group bp_core_set_avatar_globals
+	 */
+	public function test_bp_core_set_avatar_globals() {
+		$bp = buddypress();
+
+		$bp_avatar = $bp->avatar;
+		$bp->avatar = new stdClass;
+
+		bp_core_set_avatar_globals();
+
+		$expected = array(
+			'thumb_widh'            => BP_AVATAR_THUMB_WIDTH,
+			'thumb_height'          => BP_AVATAR_THUMB_HEIGHT,
+			'full_width'            => BP_AVATAR_FULL_WIDTH,
+			'full_height'           => BP_AVATAR_FULL_HEIGHT,
+			'original_max_width'    => BP_AVATAR_ORIGINAL_MAX_WIDTH,
+			'original_max_filesize' => BP_AVATAR_ORIGINAL_MAX_FILESIZE,
+			'thumb_default'         => bp_core_avatar_default_thumb(),
+			'full_default'          => bp_core_avatar_default(),
+			'upload_path'           => bp_core_avatar_upload_path(),
+			'url'                   => bp_core_avatar_url(),
+			'show_avatars'          => (bool) BP_SHOW_AVATARS,
+		);
+
+		$tested = array(
+			'thumb_widh'            => $bp->avatar->thumb->width,
+			'thumb_height'          => $bp->avatar->thumb->height,
+			'full_width'            => $bp->avatar->full->width,
+			'full_height'           => $bp->avatar->full->height,
+			'original_max_width'    => $bp->avatar->original_max_width,
+			'original_max_filesize' => $bp->avatar->original_max_filesize,
+			'thumb_default'         => $bp->avatar->thumb->default,
+			'full_default'          => $bp->avatar->full->default,
+			'upload_path'           => $bp->avatar->upload_path,
+			'url'                   => $bp->avatar->url,
+			'show_avatars'          => $bp->avatar->show_avatars,
+		);
+
+		$this->assertSame( $expected, $tested );
+
+		// reset $bp->avatar
+		$bp->avatar = $bp_avatar;
+	}
+
+	/**
 	 * @ticket BP4948
 	 */
 	function test_avatars_on_non_root_blog() {
