BP_Attachment

BP Attachment class.

Description

Extend it to manage your component’s uploads.

Source

File: bp-core/classes/class-bp-attachment.php

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
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 BuddyPress 2.3.0
     * @since BuddyPress 2.4.0 Add the $upload_dir_filter_args argument to the $arguments array
     *
     * @param array|string $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' ).
     *     @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 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->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 BuddyPress 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 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 $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', 'buddyboss' ),
            1 => __( 'The uploaded file exceeds the maximum allowed file size for this site', 'buddyboss' ),
            2 => sprintf( __( 'The uploaded file exceeds the maximum allowed file size of: %s', 'buddyboss' ), size_format( $this->original_max_filesize ) ),
            3 => __( 'The uploaded file was only partially uploaded.', 'buddyboss' ),
            4 => __( 'No file was uploaded.', 'buddyboss' ),
            5 => '',
            6 => __( 'Missing a temporary folder.', 'buddyboss' ),
            7 => __( 'Failed to write file to disk.', 'buddyboss' ),
            8 => __( 'File upload stopped by extension.', 'buddyboss' ),
        );
 
        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).
     * @param string|null $time              Optional. Time formatted in 'yyyy/mm'. Default null.
     * @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 = '', $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 BuddyPress 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.
     *
     * @see check_upload_mimes()
     *
     * @since BuddyPress 2.3.0
     *
     */
    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
     * @since BuddyPress 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 BuddyPress 2.3.0
         * @since BuddyPress 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 BuddyPress 2.3.0
     *
     */
    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 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.
     * }
     *
     * @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.', 'buddyboss' ) );
            return $wp_error;
        }
 
        // Check image file pathes.
        $path_error = __( 'Cropping the file failed: the file path is not allowed.', 'buddyboss' );
 
        // 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' => __( 'source file', 'buddyboss' ) ) );
        if ( ! empty( $r['dst_file'] ) ) {
            $check_types['dst_file'] = array( 'file' => $r['dst_file'], 'error' => __( 'destination file', 'buddyboss' ) );
        }
 
        /**
         * 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.', 'buddyboss' ), $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 BuddyPress 2.3.0
     *
     * @return array The javascript localization data.
     */
    public function script_data() {
        $script_data = 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.', 'buddyboss' ),
                2 => __( 'File successfully uploaded.', 'buddyboss' ),
            ),
        );
 
        return $script_data;
    }
 
    /**
     * Get full data for an image
     *
     * @since BuddyPress 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, $sourceImageType ) = @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( $sourceImageType ) ) {
            return false;
        }
 
        // Initialize the image data.
        $image_data = array(
            'width'  => $width,
            'height' => $height,
        );
 
        /**
         * Make sure the wp_read_image_metadata function is reachable for the old Avatar UI
         * or if WordPress < 3.9 (New Avatar UI is not available in this case)
         */
        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 BuddyPress 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 BuddyPress 2.4.0
     *
     * @param string $attachment_type The attachment type (eg: avatar or cover_image). Required.
     * @param array  $args {
     *     @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;
        }
    }
}

Changelog

Changelog
Version Description
BuddyPress 2.3.0 Introduced.

Methods

Questions?

We're always happy to help with code or other questions you might have! Search our developer docs, contact support, or connect with our sales team.