diff --git src/bp-core/bp-core-avatars.php src/bp-core/bp-core-avatars.php
index c607bb2..44fe3c3 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,79 +672,18 @@ 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 ) );
-		}
-	}
-
-
-
-	// 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 );
+	// Crop the file
+	$avatar_attachment = new BP_Attachment_Avatar();
+	$cropped           = $avatar_attachment->crop( $r );
 
 	// Check for errors
-	if ( empty( $full_cropped ) || empty( $thumb_cropped ) || is_wp_error( $full_cropped ) || is_wp_error( $thumb_cropped ) )
+	if ( empty( $cropped['full'] ) || empty( $cropped['thumb'] ) || is_wp_error( $cropped['full'] ) || is_wp_error( $cropped['thumb'] ) ) {
 		return false;
-
-	// Remove the original
-	@unlink( $original_file );
+	}
 
 	return true;
 }
@@ -926,16 +799,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 +842,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 +876,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 +887,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 70d9084..41be065 100644
--- src/bp-core/bp-core-classes.php
+++ src/bp-core/bp-core-classes.php
@@ -21,3 +21,5 @@ 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-media-extractor.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 f308211..2c246ef 100644
--- src/bp-core/bp-core-functions.php
+++ src/bp-core/bp-core-functions.php
@@ -2147,3 +2147,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..dcc63e1 100644
--- src/bp-core/classes/class-bp-attachment-avatar.php
+++ src/bp-core/classes/class-bp-attachment-avatar.php
@@ -0,0 +1,247 @@
+<?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 bp_core_avatar_original_max_filesize()
+	 * @uses BP_Attachment::__construct()
+	 */
+	public function __construct() {
+		parent::__construct( array(
+			'action'                => 'bp_avatar_upload',
+			'file_input'            => 'file',
+			'original_max_filesize' => bp_core_avatar_original_max_filesize(),
+
+			// Specific errors for avatars
+			'upload_error_strings'  => array(
+				9  => sprintf( __( 'That photo is too big. Please upload one smaller than %s', 'buddypress' ), size_format( bp_core_avatar_original_max_filesize() ) ),
+				10 => __( '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)
+	 *
+	 * @param string $file the absolute path to the file
+	 * @uses  bp_core_avatar_original_max_width()
+	 * @uses  wp_get_image_editor()
+	 * @return mixed
+	 */
+	public static function shrink( $file = '' ) {
+		// 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 string $file the absolute path to the file
+	 * @uses  bp_core_avatar_full_width()
+	 * @uses  bp_core_avatar_full_height()
+	 * @return boolean
+	 */
+	public static function is_too_small( $file = '' ) {
+		$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_Attachment::crop for the 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;
+		}
+
+		/**
+		 * 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;
+		}
+
+		// 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'] ) ) {
+			$args['crop_w'] = bp_core_avatar_full_width();
+		}
+
+		if ( empty( $args['crop_h'] ) ) {
+			$args['crop_h'] = bp_core_avatar_full_height();
+		}
+
+		// Get the file extension
+		$data = @getimagesize( $absolute_path );
+		$ext  = $data['mime'] == 'image/png' ? 'png' : 'jpg';
+
+		$args['original_file'] = $absolute_path;
+		$args['src_abs']       = false;
+		$avatar_types = array( 'full' => '', 'thumb' => '' );
+
+		foreach ( $avatar_types as $key_type => $type ) {
+			$args['dst_w']    = bp_core_avatar_dimension( $key_type, 'width' );
+			$args['dst_h']    = bp_core_avatar_dimension( $key_type, 'height' );
+			$args['dst_file'] = $avatar_folder_dir . '/' . wp_hash( $absolute_path . time() ) . '-bp' . $key_type . '.' . $ext;
+
+			$avatar_types[ $key_type ] = parent::crop( $args );
+		}
+
+		// Remove the original
+		@unlink( $absolute_path );
+
+		// Return the full and thumb cropped avatars
+		return $avatar_types;
+	}
+}
diff --git src/bp-core/classes/class-bp-attachment.php src/bp-core/classes/class-bp-attachment.php
index e69de29..05bd841 100644
--- src/bp-core/classes/class-bp-attachment.php
+++ src/bp-core/classes/class-bp-attachment.php
@@ -0,0 +1,504 @@
+<?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 = 0;
+
+	/**
+	 * 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 = '';
+
+	/**
+	 * 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. Defaults to php.ini settings.
+	 *     @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()} (required)
+	 *     @type string $file_input            The name attribute used in the file input. (required)
+	 *     @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 = '' ) {
+		// Upload action and the file input name are required parameters
+		if ( empty( $args['action'] ) || empty( $args['file_input'] ) ) {
+			return false;
+		}
+
+		// Sanitize the action ID and the file input name
+		$this->action     = sanitize_key( $args['action'] );
+		$this->file_input = sanitize_key( $args['file_input'] );
+
+		/**
+		 * Max file size defaults to php ini settings or, in the case of
+		 * a multisite config, the root site fileupload_maxk option
+		 */
+		$this->original_max_filesize = (int) wp_max_upload_size();
+
+		$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
+	 * @return array the list of upload errors
+	 */
+	public function set_upload_error_strings( $param = array() ) {
+		/**
+		 * Index of the array is the error code
+		 * Custom errors will start at 9 code
+		 */
+		$upload_errors = array(
+			0 => __( 'The file was uploaded successfully', 'buddypress' ),
+			1 => __( 'The uploaded file exceeds the maximum allowed file size for 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' ),
+		);
+
+		if ( ! array_intersect_key( $upload_errors, (array) $param ) ) {
+			foreach ( $param as $key_error => $error_message ) {
+				$upload_errors[ $key_error ] = $error_message;
+			}
+		}
+
+		return $upload_errors;
+	}
+
+	/**
+	 * Include the WordPress core needed files
+	 *
+	 * @since BuddyPress (2.3.0)
+	 */
+	public function includes() {
+		foreach ( array_unique( $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 = '' ) {
+		/**
+		 * Upload action and the file input name are required parameters
+		 * @see BP_Attachment:__construct()
+		 */
+		if ( empty( $this->action ) || empty( $this->file_input ) ) {
+			return false;
+		}
+
+		/**
+		 * 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
+	 * By default, if an original_max_filesize is provided, a check will be done
+	 * on the file size.
+	 *
+	 * @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() ) {
+		// Bail if already an error
+		if ( ! empty( $file['error'] ) ) {
+			return $file;
+		}
+
+		if ( ! empty( $this->original_max_filesize ) && $file['size'] > $this->original_max_filesize ) {
+			$file['error'] = 2;
+		}
+
+		// Return the file
+		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;
+	}
+
+	/**
+	 * Crop an image file
+	 *
+	 * @since BuddyPress (2.3.0)
+	 *
+	 * @param array $args {
+	 *     @type string $original_file The source file (absolute path) for the Attachment.
+	 *     @type int    $crop_x The start x position to crop from.
+	 *     @type int    $crop_y The start y position to crop from.
+	 *     @type int    $crop_w The width to crop.
+	 *     @type int    $crop_h The height to crop.
+	 *     @type int    $dst_w The destination width.
+	 *     @type int    $dst_h The destination height.
+	 *     @type int    $src_abs Optional. If the source crop points are absolute.
+	 *     @type string $dst_file Optional. The destination file to write to.
+	 * }
+	 * @uses wp_crop_image()
+	 * @return string|WP_Error New filepath on success, WP_Error on failure.
+	 */
+	public function crop( $args = array() ) {
+		$wp_error = new WP_Error();
+
+		$r = wp_parse_args( $args, array(
+			'original_file' => '',
+			'crop_x'        => 0,
+			'crop_y'        => 0,
+			'crop_w'        => 0,
+			'crop_h'        => 0,
+			'dst_w'         => 0,
+			'dst_h'         => 0,
+			'src_abs'       => false,
+			'dst_file'      => false,
+		) );
+
+		if ( empty( $r['original_file'] ) || ! file_exists( $r['original_file'] ) ) {
+			$wp_error->add( 'crop_error', __( 'Cropping the file failed: missing source file.', 'buddypress' ) );
+			return $wp_error;
+		}
+
+		// Check image file pathes
+		$path_error = __( 'Cropping the file failed: the file path is not allowed.', 'buddypress' );
+
+		// Make sure it's coming from an uploaded file
+		if ( false === strpos( $r['original_file'], $this->upload_path ) ) {
+			$wp_error->add( 'crop_error', $path_error );
+			return $wp_error;
+		}
+
+		/**
+		 * If no destination file is provided, WordPress will use a default name
+		 * and will write the file in the source file's folder.
+		 * If a destination file is provided, we need to make sure it's going into uploads
+		 */
+		if ( ! empty( $r['dst_file'] ) && false === strpos( $r['dst_file'], $this->upload_path ) ) {
+			$wp_error->add( 'crop_error', $path_error );
+			return $wp_error;
+		}
+
+		// Check image file types
+		$check_types = array( 'src_file' => array( 'file' => $r['original_file'], 'error' => _x( 'source file', 'Attachment source file', 'buddypress' ) ) );
+		if ( ! empty( $r['dst_file'] ) ) {
+			$check_types['dst_file'] = array( 'file' => $r['dst_file'], 'error' => _x( 'destination file', 'Attachment destination file', 'buddypress' ) );
+		}
+
+		/**
+		 * WordPress image supported types
+		 * @see wp_attachment_is()
+		 */
+		$supported_image_types = array(
+			'jpg'  => 1,
+			'jpeg' => 1,
+			'jpe'  => 1,
+			'gif'  => 1,
+			'png'  => 1,
+		);
+
+		foreach ( $check_types as $file ) {
+			$is_image      = wp_check_filetype( $file['file'] );
+			$ext           = $is_image['ext'];
+
+			if ( empty( $ext ) || empty( $supported_image_types[ $ext ] ) ) {
+				$wp_error->add( 'crop_error', sprintf( __( 'Cropping the file failed: %s is not a supported image file.', 'buddypress' ), $file['error'] ) );
+				return $wp_error;
+			}
+		}
+
+		// Add the image.php to the required WordPress files, if it's not already the case
+		$required_files = array_flip( $this->required_wp_files );
+		if ( ! isset( $required_files['image'] ) ) {
+			$this->required_wp_files[] = 'image';
+		}
+
+		// Load the files
+		$this->includes();
+
+		// Finally crop the image
+		return wp_crop_image( $r['original_file'], (int) $r['crop_x'], (int) $r['crop_y'], (int) $r['crop_w'], (int) $r['crop_h'], (int) $r['dst_w'], (int) $r['dst_h'], $r['src_abs'], $r['dst_file'] );
+	}
+}
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 1a46340..01e9772 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/assets/attachment-extensions.php tests/phpunit/assets/attachment-extensions.php
index e69de29..117b456 100644
--- tests/phpunit/assets/attachment-extensions.php
+++ tests/phpunit/assets/attachment-extensions.php
@@ -0,0 +1,10 @@
+<?php
+/**
+ * The following implementations of BP_Attachment act as dummy plugins
+ * for our unit tests
+ */
+class BP_Attachment_Extend extends BP_Attachment {
+	public function __construct( $args = array() ) {
+		return parent::__construct( $args );
+	}
+}
diff --git tests/phpunit/testcases/core/class-bp-attachment-avatar.php tests/phpunit/testcases/core/class-bp-attachment-avatar.php
index e69de29..d719dd3 100644
--- tests/phpunit/testcases/core/class-bp-attachment-avatar.php
+++ tests/phpunit/testcases/core/class-bp-attachment-avatar.php
@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * @group core
+ * @group avatars
+ * @group bp_attachments
+ * @group BP_Attachement_Avatar
+ */
+class BP_Tests_BP_Attachement_Avatar_TestCases extends BP_UnitTestCase {
+	protected $displayed_user;
+
+	public function setUp() {
+		parent::setUp();
+		$bp = buddypress();
+		$this->displayed_user = $bp->displayed_user;
+		$bp->displayed_user = new stdClass;
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		buddypress()->displayed_user = $this->displayed_user;
+	}
+
+	public function max_filesize() {
+		return 1000;
+	}
+
+	private function clean_avatars( $type = 'user' ) {
+		if ( 'user' === $type ) {
+			$avatar_dir = 'avatars';
+		} elseif ( 'group' === $type ) {
+			$avatar_dir = 'group-avatars';
+		}
+
+		$this->rrmdir( bp_core_avatar_upload_path() . '/' . $avatar_dir );
+	}
+
+	private function rrmdir( $dir ) {
+		$d = glob( $dir . '/*' );
+
+		if ( empty( $d ) ) {
+			return;
+		}
+
+		foreach ( $d as $file ) {
+			if ( is_dir( $file ) ) {
+				$this->rrmdir( $file );
+			} else {
+				@unlink( $file );
+			}
+		}
+
+		@rmdir( $dir );
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_user_avatar_no_error() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$u1 = $this->factory->user->create();
+		buddypress()->displayed_user->id = $u1;
+
+		$avatar = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+
+		$this->assertTrue( empty( $user_avatar['error'] ) );
+
+		// clean up!
+		$this->clean_avatars();
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_group_avatar_no_error() {
+		$bp = buddypress();
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+		$reset_current_group = $bp->groups->current_group;
+
+		$g = $this->factory->group->create();
+
+		$bp->groups->current_group = groups_get_group( array(
+			'group_id'        => $g,
+			'populate_extras' => true,
+		) );
+
+		$avatar = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$group_avatar = $avatar_attachment->upload( $_FILES, 'groups_avatar_upload_dir' );
+
+		$this->assertTrue( empty( $group_avatar['error'] ) );
+
+		// clean up!
+		$this->clean_avatars( 'group' );
+		$bp->groups->current_group = $reset_current_group;
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_user_avatar_file_size_error() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$u1 = $this->factory->user->create();
+		buddypress()->displayed_user->id = $u1;
+
+		$avatar = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		add_filter( 'bp_core_avatar_original_max_filesize', array( $this, 'max_filesize' ) );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+
+		remove_filter( 'bp_core_avatar_original_max_filesize', array( $this, 'max_filesize' ) );
+
+		$this->assertFalse( empty( $user_avatar['error'] ) );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload_user_avatar_file_type_error() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$u1 = $this->factory->user->create();
+		buddypress()->displayed_user->id = $u1;
+
+		$avatar = BP_TESTS_DIR . 'assets/files/buddypress_logo.pdf';
+
+		$tmp_name = wp_tempnam( $avatar );
+
+		copy( $avatar, $tmp_name );
+
+		// Upload the file
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$_POST['action'] = $avatar_attachment->action;
+		$_FILES[ $avatar_attachment->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'buddypress_logo.pdf',
+			'type'     => 'application/pdf',
+			'error'    => 0,
+			'size'     => filesize( $avatar )
+		);
+
+		$user_avatar = $avatar_attachment->upload( $_FILES, 'xprofile_avatar_upload_dir' );
+
+		$this->assertFalse( empty( $user_avatar['error'] ) );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	/**
+	 * @group crop
+	 */
+	public function test_crop_avatar() {
+		$avatar = BP_TESTS_DIR . 'assets/files/buddypress-book.jpg';
+		$pdf    = BP_TESTS_DIR . 'assets/files/buddypress_logo.pdf';
+
+		$u1 = $this->factory->user->create();
+
+		$upload_dir = xprofile_avatar_upload_dir( 'avatars', $u1 );
+
+		wp_mkdir_p( $upload_dir['path'] );
+
+		copy( $avatar, $upload_dir['path'] . '/buddypress-book.jpg' );
+		copy( $pdf, $upload_dir['path'] . '/buddypress_logo.pdf' );
+
+		$crop_args = array(
+			'object'        => 'user',
+			'avatar_dir'    => 'avatars',
+			'item_id'       => $u1,
+			'original_file' => '/avatars/' . $u1 . '/buddypress-book.jpg',
+		);
+
+		$avatar_attachment = new BP_Attachment_Avatar();
+		$cropped = $avatar_attachment->crop( $crop_args );
+
+		$this->assertTrue( ! empty( $cropped['full'] ) && ! is_wp_error( $cropped['full'] ) );
+
+		$crop_args['original_file'] = '/avatars/' . $u1 . '/buddypress_logo.pdf';
+
+		$cropped = $avatar_attachment->crop( $crop_args );
+		$this->assertTrue( is_wp_error( $cropped['full'] ) );
+
+		// Clean up
+		$this->clean_avatars();
+	}
+}
diff --git tests/phpunit/testcases/core/class-bp-attachment.php tests/phpunit/testcases/core/class-bp-attachment.php
index e69de29..2b97b2f 100644
--- tests/phpunit/testcases/core/class-bp-attachment.php
+++ tests/phpunit/testcases/core/class-bp-attachment.php
@@ -0,0 +1,175 @@
+<?php
+
+include_once BP_TESTS_DIR . '/assets/attachment-extensions.php';
+
+/**
+ * @group bp_attachments
+ * @group BP_Attachement
+ */
+class BP_Tests_BP_Attachment_TestCases extends BP_UnitTestCase {
+
+	private function clean_files( $basedir = 'attachment_base_dir' ) {
+		$upload_dir = bp_upload_dir();
+
+		$this->rrmdir( $upload_dir['basedir'] . '/' . $basedir );
+	}
+
+	private function rrmdir( $dir ) {
+		$d = glob( $dir . '/*' );
+
+		if ( empty( $d ) ) {
+			return;
+		}
+
+		foreach ( $d as $file ) {
+			if ( is_dir( $file ) ) {
+				$this->rrmdir( $file );
+			} else {
+				@unlink( $file );
+			}
+		}
+
+		@rmdir( $dir );
+	}
+
+	public function test_construct_missing_required_parameter() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$_FILES['file'] = array(
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => 10000
+		);
+
+		$attachment_class = new BP_Attachment_Extend();
+		$upload = $attachment_class->upload( $_FILES );
+
+		$this->assertTrue( empty( $upload ) );
+
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+	}
+
+	public function test_set_upload_dir() {
+		$upload_dir = bp_upload_dir();
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'     => 'attachment_action',
+			'file_input' => 'attachment_file_input'
+		) );
+
+		$this->assertSame( $attachment_class->upload_dir, bp_upload_dir() );
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'     => 'attachment_action',
+			'file_input' => 'attachment_file_input',
+			'base_dir'   => 'attachment_base_dir',
+		) );
+
+		$this->assertTrue( file_exists( $upload_dir['basedir'] . '/attachment_base_dir'  ) );
+
+		// clean up
+		$this->clean_files();
+	}
+
+	/**
+	 * @group upload
+	 */
+	public function test_upload() {
+		$reset_files = $_FILES;
+		$reset_post = $_POST;
+
+		$file = BP_TESTS_DIR . 'assets/files/disc.png';
+
+		$tmp_name = wp_tempnam( $file );
+
+		copy( $file, $tmp_name );
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'                => 'attachment_action',
+			'file_input'            => 'attachment_file_input',
+			'base_dir'   		    => 'attachment_base_dir',
+			'original_max_filesize' => 10000,
+		) );
+
+		$_POST['action'] = $attachment_class->action;
+		$_FILES[ $attachment_class->file_input ] = array(
+			'tmp_name' => $tmp_name,
+			'name'     => 'disc.png',
+			'type'     => 'image/png',
+			'error'    => 0,
+			'size'     => filesize( $file )
+		);
+
+		// Error: file size
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertFalse( empty( $upload['error'] ) );
+
+		$attachment_class->allowed_mime_types    = array( 'pdf' );
+		$attachment_class->original_max_filesize = false;
+
+		// Error: file type
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertFalse( empty( $upload['error'] ) );
+
+		$attachment_class->allowed_mime_types = array();
+
+		// Success
+		$upload = $attachment_class->upload( $_FILES );
+		$this->assertTrue( empty( $upload['error'] ) );
+
+		// clean up!
+		$_FILES = $reset_files;
+		$_POST = $reset_post;
+		$this->clean_files();
+	}
+
+	/**
+	 * @group crop
+	 */
+	public function test_crop_image() {
+		$image = BP_TESTS_DIR . 'assets/files/buddypress-book.jpg';
+
+		$crop_args = array(
+			'original_file' => $image,
+			'crop_x'        => 0,
+			'crop_y'        => 0,
+			'crop_w'        => 150,
+			'crop_h'        => 150,
+			'dst_w'         => 150,
+			'dst_h'         => 150,
+		);
+
+		$attachment_class = new BP_Attachment_Extend( array(
+			'action'                => 'attachment_action',
+			'file_input'            => 'attachment_file_input',
+			'base_dir'   		    => 'attachment_base_dir',
+		) );
+		$cropped = $attachment_class->crop( $crop_args );
+
+		// Image must come from the upload basedir
+		$this->assertTrue( is_wp_error( $cropped ) );
+
+		$crop_args['original_file'] = $attachment_class->upload_path . '/buddypress-book.jpg';
+
+		// Move to the base upload dir
+		copy( $image, $crop_args['original_file'] );
+
+		// Image must stay in the upload basedir
+		$crop_args['dst_file'] = BP_TESTS_DIR . 'assets/files/error.jpg';
+		$cropped = $attachment_class->crop( $crop_args );
+
+		// Image must stay in the upload basedir
+		$this->assertTrue( is_wp_error( $cropped ) );
+
+		unset( $crop_args['dst_file'] );
+
+		$cropped = $attachment_class->crop( $crop_args );
+		$this->assertFalse( is_wp_error( $cropped ) );
+
+		// clean up!
+		$this->clean_files();
+	}
+}
diff --git tests/phpunit/testcases/core/functions.php tests/phpunit/testcases/core/functions.php
index 1ab3b45..d165739 100644
--- tests/phpunit/testcases/core/functions.php
+++ tests/phpunit/testcases/core/functions.php
@@ -572,4 +572,26 @@ class BP_Tests_Core_Functions extends BP_UnitTestCase {
 			date_default_timezone_set( $tz_backup );
 		}
 	}
+
+	/**
+	 * @group bp_attachments
+	 * @group bp_upload_dir
+	 */
+	public function test_bp_upload_dir_ms() {
+		if ( ! is_multisite() ) {
+			return;
+		}
+
+		$expected_upload_dir = wp_upload_dir();
+
+		$b = $this->factory->blog->create();
+
+		switch_to_blog( $b );
+
+		$tested_upload_dir = bp_upload_dir();
+
+		restore_current_blog();
+
+		$this->assertSame( $expected_upload_dir, $tested_upload_dir );
+	}
 }
