File: /var/www/intranet.kauko.lt/wp-content/plugins/buddypress/bp-core/classes/class-bp-attachment.php
<?php
/**
* Core attachment class.
*
* @package BuddyPress
* @subpackage Core
* @since 2.3.0
*/
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* BP Attachment class.
*
* Extend it to manage your component's uploads.
*
* @since 2.3.0
*/
abstract class BP_Attachment {
/** Upload properties *****************************************************/
/**
* The file being uploaded.
*
* @var array
*/
public $attachment = array();
/**
* The default args to be merged with the
* ones passed by the child class.
*
* @var array
*/
protected $default_args = array(
'original_max_filesize' => 0,
'allowed_mime_types' => array(),
'base_dir' => '',
'action' => '',
'file_input' => '',
'upload_error_strings' => array(),
'required_wp_files' => array( 'file' ),
'upload_dir_filter_args' => 0,
);
/**
* Construct Upload parameters.
*
* @since 2.3.0
* @since 2.4.0 Add the $upload_dir_filter_args argument to the $arguments array
*
* @param array|string $args {
* Array of upload parameters.
*
* @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' ).
* @type int $upload_dir_filter_args 1 to receive the original Upload dir array in the Upload dir filter, 0 otherwise.
* Defaults to 0 (optional).
* }
*/
public function __construct( $args = '' ) {
// Upload action and the file input name are required parameters.
if ( empty( $args['action'] ) || empty( $args['file_input'] ) ) {
return;
}
// 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->default_args['original_max_filesize'] = (int) wp_max_upload_size();
$params = bp_parse_args(
$args,
$this->default_args,
$this->action . '_upload_params'
);
foreach ( $params as $key => $param ) {
if ( 'upload_error_strings' === $key ) {
$this->{$key} = $this->set_upload_error_strings( $param );
// Sanitize the base dir.
} elseif ( 'base_dir' === $key ) {
$this->{$key} = sanitize_title( $param );
// Sanitize the upload dir filter arg to pass.
} elseif ( 'upload_dir_filter_args' === $key ) {
$this->{$key} = (int) $param;
// Action & File input are already set and sanitized.
} elseif ( 'action' !== $key && 'file_input' !== $key ) {
$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 2.3.0
*/
public function set_upload_dir() {
// Set the directory, path, & url variables.
$this->upload_dir = bp_upload_dir();
if ( empty( $this->upload_dir ) ) {
return;
}
$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 2.3.0
*
* @param array $param A list of error messages to add to BuddyPress core ones.
* @return array $upload_errors 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' ),
/* translators: %s: Max file size for the file */
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 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 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).
* @param string|null $time Optional. Time formatted in 'yyyy/mm'. Default null.
* @return false|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 = '', $time = null ) {
/**
* 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.
*
* @param 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.
if ( ! empty( $upload_dir_filter ) ) {
add_filter( 'upload_dir', $upload_dir_filter, 10, $this->upload_dir_filter_args );
}
// Helper for utf-8 filenames.
add_filter( 'sanitize_file_name', array( $this, 'sanitize_utf8_filename' ) );
// Upload the attachment.
$this->attachment = wp_handle_upload( $file[ $this->file_input ], $overrides, $time );
remove_filter( 'sanitize_file_name', array( $this, 'sanitize_utf8_filename' ) );
// Restore WordPress Uploads data.
if ( ! empty( $upload_dir_filter ) ) {
remove_filter( 'upload_dir', $upload_dir_filter, 10 );
}
// Finally return the uploaded file or the error.
return $this->attachment;
}
/**
* Helper to convert utf-8 characters in filenames to their ASCII equivalent.
*
* @since 2.9.0
*
* @param string $retval Filename.
* @return string
*/
public function sanitize_utf8_filename( $retval ) {
// PHP 5.4+ or with PECL intl 2.0+ .
if ( function_exists( 'transliterator_transliterate' ) && seems_utf8( $retval ) ) {
$retval = transliterator_transliterate( 'Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove', $retval );
// Older.
} else {
// Use WP's built-in function to convert accents to their ASCII equivalent.
$retval = remove_accents( $retval );
// Still here? use iconv().
if ( function_exists( 'iconv' ) && seems_utf8( $retval ) ) {
$retval = iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $retval );
}
}
return $retval;
}
/**
* 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.
*
* @since 2.3.0
*
* @see check_upload_mimes()
*
* @return array Valid 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 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 2.3.0
* @since 2.4.0 Add the $upload_dir parameter to the method
*
* regarding to context
*
* @param array $upload_dir The original Uploads dir.
* @return array The upload directory data.
*/
public function upload_dir_filter( $upload_dir = array() ) {
/**
* Filters the component's upload directory.
*
* @since 2.3.0
* @since 2.4.0 Include the original Upload directory as the second parameter of the filter.
*
* @param array $value Array containing the path, URL, and other helpful settings.
* @param array $upload_dir The original Uploads dir.
*/
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,
),
$upload_dir
);
}
/**
* 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 2.3.0
*
* @return bool
*/
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 ( ! is_dir( $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 2.3.0
*
* @param array $args {
* Array of arguments for the crop method.
*
* @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.
* }
*
* @return string|WP_Error New filepath on success, WP_Error on failure.
*/
public function crop( $args = array() ) {
$wp_error = new WP_Error();
$r = bp_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,
),
'bp_attachment_crop_args'
);
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' ),
);
}
// Set supported image types.
$supported_image_types = array_fill_keys( bp_attachments_get_allowed_types( 'image' ), 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(
/* translators: %s: image file extension */
__( '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'] );
}
/**
* Build script datas for the Uploader UI.
*
* Override this method from your child class to build the script datas.
*
* @since 2.3.0
*
* @return array The javascript localization data.
*/
public function script_data() {
return array(
'action' => $this->action,
'file_data_name' => $this->file_input,
'max_file_size' => $this->original_max_filesize,
'feedback_messages' => array(
1 => __( 'Sorry, uploading the file failed.', 'buddypress' ),
2 => __( 'File successfully uploaded.', 'buddypress' ),
),
);
}
/**
* Adds a new revision of a file.
*
* @since 10.0.0
*
* @param string $attachment_type The attachement type (eg: avatar).
* @param array $args {
* Optional. Array of arguments for the add_revision method.
*
* @type string $file_abspath The source file (absolute path) for the attachment.
* @type string $file_id Optional. The file ID to use as a suffix for the revision directory.
* }
* @return object|WP_Error An object informing about the URL an Path to a revision file, a WP_Error object on failure.
*/
public function add_revision( $attachment_type, $args = array() ) {
$r = bp_parse_args(
$args,
array(
'file_abspath' => '',
'file_id' => '',
),
'attachment_' . $attachment_type . '_add_revision'
);
if ( ! $r['file_abspath'] ) {
return new WP_Error( 'missing_parameter', __( 'The absolute path to your file is missing.', 'buddypress' ) );
// Make sure it's coming from an uploaded file.
} elseif ( false === strpos( $r['file_abspath'], $this->upload_path ) ) {
return new WP_Error( 'forbidden_path', __( 'The absolute path to your file is not allowed.', 'buddypress' ) );
} else {
$filepath = $r['file_abspath'];
}
$dirname = trailingslashit( dirname( $filepath ) );
$filename = sanitize_file_name( wp_basename( $filepath ) );
if ( ! $r['file_id'] ) {
$r['file_id'] = $filename;
}
$file_id = wp_hash( $r['file_id'] );
// Set the revision name & dir.
$revision_name = '';
$revision_dir = $dirname . '._revisions_' . $file_id;
// Avatars and Cover Images are specific attachments.
if ( 'avatar' === $attachment_type || 'cover_image' === $attachment_type ) {
$revision_dir = $dirname . 'history';
}
// Create the revision directory if it doesn't exist yet.
if ( ! is_dir( $revision_dir ) ) {
mkdir( $revision_dir );
}
$revision_name = wp_unique_filename( $revision_dir, $filename );
$revision_path = trailingslashit( $revision_dir ) . $revision_name;
if ( ! rename( $filepath, $revision_path ) ) {
return new WP_Error( 'adding_revision_failed', __( 'An unexpected error occured while adding the revision.', 'buddypress' ) );
}
return (object) array(
'url' => str_replace( trailingslashit( $this->upload_path ), trailingslashit( $this->url ), $revision_path ),
'path' => $revision_path,
);
}
/**
* Get full data for an image
*
* @since 2.4.0
*
* @param string $file Absolute path to the uploaded image.
* @return bool|array An associate array containing the width, height and metadatas.
* False in case an important image attribute is missing.
*/
public static function get_image_data( $file ) {
// Try to get image basic data.
list( $width, $height, $source_image_type ) = @getimagesize( $file );
// No need to carry on if we couldn't get image's basic data.
if ( is_null( $width ) || is_null( $height ) || is_null( $source_image_type ) ) {
return false;
}
// Initialize the image data.
$image_data = array(
'width' => $width,
'height' => $height,
);
// Make sure the wp_read_image_metadata function is reachable.
if ( ! function_exists( 'wp_read_image_metadata' ) ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
// Now try to get image's meta data.
$meta = wp_read_image_metadata( $file );
if ( ! empty( $meta ) ) {
$image_data['meta'] = $meta;
}
/**
* Filter here to add/remove/edit data to the image full data
*
* @since 2.4.0
*
* @param array $image_data An associate array containing the width, height and metadatas.
*/
return apply_filters( 'bp_attachments_get_image_data', $image_data );
}
/**
* Edit an image file to resize it or rotate it
*
* @since 2.4.0
*
* @param string $attachment_type The attachment type (eg: avatar or cover_image). Required.
* @param array $args {
* Optional. Array of arguments for the edit_image method.
*
* @type string $file Absolute path to the image file (required).
* @type int $max_w Max width attribute for the editor's resize method (optional).
* @type int $max_h Max height attribute for the editor's resize method (optional).
* @type bool $crop Crop attribute for the editor's resize method (optional).
* @type float $rotate Angle for the editor's rotate method (optional).
* @type int $quality Compression quality on a 1-100% scale (optional).
* @type bool $save Whether to use the editor's save method or not (optional).
* }
* @return string|WP_Image_Editor|WP_Error The edited image path or the WP_Image_Editor object in case of success,
* an WP_Error object otherwise.
*/
public static function edit_image( $attachment_type, $args = array() ) {
if ( empty( $attachment_type ) ) {
return new WP_Error( 'missing_parameter' );
}
$r = bp_parse_args(
$args,
array(
'file' => '',
'max_w' => 0,
'max_h' => 0,
'crop' => false,
'rotate' => 0,
'quality' => 90,
'save' => true,
),
'attachment_' . $attachment_type . '_edit_image'
);
// Make sure we have to edit the image.
if ( empty( $r['max_w'] ) && empty( $r['max_h'] ) && empty( $r['rotate'] ) && empty( $r['file'] ) ) {
return new WP_Error( 'missing_parameter' );
}
// Get the image editor.
$editor = wp_get_image_editor( $r['file'] );
if ( is_wp_error( $editor ) ) {
return $editor;
}
$editor->set_quality( $r['quality'] );
if ( ! empty( $r['rotate'] ) ) {
$rotated = $editor->rotate( $r['rotate'] );
// Stop in case of error.
if ( is_wp_error( $rotated ) ) {
return $rotated;
}
}
if ( ! empty( $r['max_w'] ) || ! empty( $r['max_h'] ) ) {
$resized = $editor->resize( $r['max_w'], $r['max_h'], $r['crop'] );
// Stop in case of error.
if ( is_wp_error( $resized ) ) {
return $resized;
}
}
// Use the editor save method to get a path to the edited image.
if ( true === $r['save'] ) {
return $editor->save( $editor->generate_filename() );
// Need to do some other edit actions or use a specific method to save file.
} else {
return $editor;
}
}
}