├── LICENSE ├── network-media-library.php └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Frank Bültge 4 | Copyright (c) 2018 Human Made 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /network-media-library.php: -------------------------------------------------------------------------------- 1 | , Dominik Schilling , Frank Bültge 15 | * @copyright 2019 Human Made 16 | * @license https://opensource.org/licenses/MIT 17 | * 18 | * Plugin Name: Network Media Library 19 | * Description: Network Media Library provides a central media library that's shared across all sites on the Multisite network. 20 | * Network: true 21 | * Plugin URI: https://github.com/humanmade/network-media-library 22 | * Version: 1.5.0 23 | * Author: John Blackbourn, Dominik Schilling, Frank Bültge 24 | * Author URI: https://github.com/humanmade/network-media-library/graphs/contributors 25 | * License: MIT 26 | * License URI: ./LICENSE 27 | * Text Domain: network-media-library 28 | * Domain Path: /languages 29 | * Requires PHP: 7.0 30 | */ 31 | 32 | declare( strict_types=1 ); 33 | 34 | namespace Network_Media_Library; 35 | 36 | use WP_Post; 37 | 38 | /** 39 | * Don't call this file directly. 40 | */ 41 | defined( 'ABSPATH' ) || die(); 42 | 43 | /** 44 | * Don't run if multisite not enabled 45 | */ 46 | if ( ! is_multisite() ) { 47 | return; 48 | } 49 | 50 | /** 51 | * The ID of the site on the network which acts as the network media library. Change this value with the help 52 | * of the filter hook `network-media-library/site_id`. 53 | * 54 | * @var int The network media library site ID. 55 | */ 56 | const SITE_ID = 2; 57 | 58 | /** 59 | * Returns the ID of the site which acts as the network media library. 60 | * 61 | * @return int The network media library site ID. 62 | */ 63 | function get_site_id() : int { 64 | $site_id = SITE_ID; 65 | 66 | /** 67 | * Filters the ID of the site which acts as the network media library. 68 | * 69 | * @since 1.0.0 70 | * 71 | * @param int $site_id The network media library site ID. 72 | */ 73 | $site_id = (int) apply_filters( 'network-media-library/site_id', $site_id ); 74 | 75 | /** 76 | * Legacy filter which filters the ID of the site which acts as the network media library. 77 | * 78 | * This is provided for compatibility with the Multisite Global Media plugin. 79 | * 80 | * @since 0.0.3 81 | * 82 | * @param int $site_id The network media library site ID. 83 | */ 84 | $site_id = (int) apply_filters_deprecated( 'global_media.site_id', [ $site_id ], '1.0.0', 'network-media-library/site_id' ); 85 | 86 | return $site_id; 87 | } 88 | 89 | /** 90 | * Switches the current site ID to the network media library site ID. 91 | * 92 | * @param mixed $value An optional value used when this function is used as a hook filter. 93 | * @return mixed The value of the `$value` parameter. 94 | */ 95 | function switch_to_media_site( $value = null ) { 96 | switch_to_blog( get_site_id() ); 97 | 98 | return $value; 99 | } 100 | 101 | /** 102 | * Returns whether or not we're currently on the network media library site, regardless of any switching that's occurred. 103 | * 104 | * `$current_blog` can be used to determine the "actual" site as it doesn't change when switching sites. 105 | * 106 | * @return bool Whether we're on the network media library site. 107 | */ 108 | function is_media_site() : bool { 109 | return ( get_site_id() === (int) $GLOBALS['current_blog']->blog_id ); 110 | } 111 | 112 | /** 113 | * Prevents attempts to attach an attachment to a post ID during upload. 114 | */ 115 | function prevent_attaching() { 116 | unset( $_REQUEST['post_id'] ); 117 | } 118 | 119 | add_filter( 'admin_post_thumbnail_html', __NAMESPACE__ . '\admin_post_thumbnail_html', 99, 3 ); 120 | /** 121 | * Filters the admin post thumbnail HTML markup to return. 122 | * 123 | * @param string $content Admin post thumbnail HTML markup. 124 | * @param int $post_id Post ID. 125 | * @param int|null $thumbnail_id Thumbnail attachment ID, or null if there isn't one. 126 | */ 127 | function admin_post_thumbnail_html( string $content, $post_id, $thumbnail_id ) : string { 128 | static $switched = false; 129 | 130 | if ( $switched ) { 131 | return $content; 132 | } 133 | 134 | if ( ! $thumbnail_id ) { 135 | return $content; 136 | } 137 | 138 | switch_to_blog( get_site_id() ); 139 | $switched = true; 140 | // $thumbnail_id is passed instead of post_id to avoid warning messages of nonexistent post object. 141 | $content = _wp_post_thumbnail_html( $thumbnail_id, $thumbnail_id ); 142 | $switched = false; 143 | restore_current_blog(); 144 | 145 | $post = get_post( $post_id ); 146 | $post_type_object = get_post_type_object( $post->post_type ); 147 | $has_thumbnail_url = get_the_post_thumbnail_url( $post_id ) !== false; 148 | 149 | if ( false === $has_thumbnail_url ) { 150 | $search = 'class="thickbox">'; 151 | $replace = 'class="thickbox">' . esc_html( $post_type_object->labels->set_featured_image ) . ''; 152 | } else { 153 | $search = '

'; 154 | $replace = '

' . esc_html( $post_type_object->labels->remove_featured_image ) . '

'; 155 | } 156 | 157 | $content = str_replace( $search, $replace, $content ); 158 | 159 | return $content; 160 | } 161 | 162 | /** 163 | * Filters the image src result so its URL points to the network media library site. 164 | * 165 | * @param array|false $image Either array with src, width & height, icon src, or false. 166 | * @param int $attachment_id Image attachment ID. 167 | * @param string|array $size Size of image. Image size or array of width and height values. 168 | * @param bool $icon Whether the image should be treated as an icon. 169 | * @return array|false Either array with src, width & height, icon src, or false. 170 | */ 171 | add_filter( 'wp_get_attachment_image_src', function( $image, $attachment_id, $size, bool $icon ) { 172 | static $switched = false; 173 | 174 | if ( $switched ) { 175 | return $image; 176 | } 177 | 178 | if ( is_media_site() ) { 179 | return $image; 180 | } 181 | 182 | switch_to_media_site(); 183 | 184 | $switched = true; 185 | $image = wp_get_attachment_image_src( $attachment_id, $size, $icon ); 186 | $switched = false; 187 | 188 | restore_current_blog(); 189 | 190 | return $image; 191 | }, 999, 4 ); 192 | 193 | /** 194 | * Filters the default gallery shortcode output so it shows media from the network media library site. 195 | * 196 | * @param string $output The gallery output. 197 | * @param array $attr Attributes of the gallery shortcode. 198 | * @param int $instance Unique numeric ID of this gallery shortcode instance. 199 | * @return string The gallery output. 200 | */ 201 | function filter_post_gallery( string $output, array $attr, int $instance ) : string { 202 | remove_filter( 'post_gallery', __NAMESPACE__ . '\filter_post_gallery', 0 ); 203 | 204 | switch_to_media_site(); 205 | $output = gallery_shortcode( $attr ); 206 | restore_current_blog(); 207 | 208 | add_filter( 'post_gallery', __NAMESPACE__ . '\filter_post_gallery', 0, 3 ); 209 | 210 | return $output; 211 | } 212 | add_filter( 'post_gallery', __NAMESPACE__ . '\filter_post_gallery', 0, 3 ); 213 | 214 | // Allow users to upload attachments. 215 | add_action( 'load-async-upload.php', __NAMESPACE__ . '\switch_to_media_site', 0 ); 216 | add_action( 'wp_ajax_upload-attachment', __NAMESPACE__ . '\switch_to_media_site', 0 ); 217 | 218 | // Allow attachments to be uploaded without a corresponding post on the network media library site. 219 | add_action( 'load-async-upload.php', __NAMESPACE__ . '\prevent_attaching', 0 ); 220 | add_action( 'wp_ajax_upload-attachment', __NAMESPACE__ . '\prevent_attaching', 0 ); 221 | 222 | // Allow access to the "List" mode on the Media screen. 223 | add_action( 'parse_request', function() { 224 | if ( is_media_site() ) { 225 | return; 226 | } 227 | 228 | if ( ! function_exists( 'get_current_screen' ) || 'upload' !== get_current_screen()->id ) { 229 | return; 230 | } 231 | 232 | switch_to_media_site(); 233 | 234 | add_filter( 'posts_pre_query', function( $value ) { 235 | restore_current_blog(); 236 | 237 | return $value; 238 | } ); 239 | 240 | add_action( 'loop_start', __NAMESPACE__ . '\switch_to_media_site', 0 ); 241 | add_action( 'loop_stop', 'restore_current_blog', 999 ); 242 | 243 | }, 0 ); 244 | 245 | // Allow attachment details to be fetched and saved. 246 | add_action( 'wp_ajax_get-attachment', __NAMESPACE__ . '\switch_to_media_site', 0 ); 247 | add_action( 'wp_ajax_save-attachment', __NAMESPACE__ . '\switch_to_media_site', 0 ); 248 | add_action( 'wp_ajax_save-attachment-compat', __NAMESPACE__ . '\switch_to_media_site', 0 ); 249 | add_action( 'wp_ajax_set-attachment-thumbnail', __NAMESPACE__ . '\switch_to_media_site', 0 ); 250 | 251 | // Allow images to be edited and previewed. 252 | add_action( 'wp_ajax_image-editor', __NAMESPACE__ . '\switch_to_media_site', 0 ); 253 | add_action( 'wp_ajax_imgedit-preview', __NAMESPACE__ . '\switch_to_media_site', 0 ); 254 | add_action( 'wp_ajax_crop-image', __NAMESPACE__ . '\switch_to_media_site', 0 ); 255 | 256 | // Allow attachments to be queried and inserted. 257 | add_action( 'wp_ajax_query-attachments', __NAMESPACE__ . '\switch_to_media_site', 0 ); 258 | add_action( 'wp_ajax_send-attachment-to-editor', __NAMESPACE__ . '\switch_to_media_site', 0 ); 259 | add_filter( 'map_meta_cap', __NAMESPACE__ . '\allow_media_library_access', 10, 4 ); 260 | 261 | // Support for the WP User Avatars plugin. 262 | add_action( 'wp_ajax_assign_wp_user_avatars_media', __NAMESPACE__ . '\switch_to_media_site', 0 ); 263 | 264 | /** 265 | * Filters the attachment data prepared for JavaScript. 266 | * 267 | * @param array $response Array of prepared attachment data. 268 | * @param WP_Post $attachment Attachment ID or object. 269 | * @param array|bool $meta Array of attachment meta data, or boolean false if there is none. 270 | * @return array Array of prepared attachment data. 271 | */ 272 | add_filter( 'wp_prepare_attachment_for_js', function( array $response, \WP_Post $attachment, $meta ) : array { 273 | if ( is_media_site() ) { 274 | return $response; 275 | } 276 | 277 | // Prevent media from being deleted from any site other than the network media library site. 278 | // This is needed in order to prevent incorrect posts from being deleted on the local site. 279 | unset( $response['nonces']['delete'] ); 280 | 281 | return $response; 282 | }, 0, 3 ); 283 | 284 | /** 285 | * Filters the pre-dispatch value of REST API requests in order to switch to the network media library site when querying media. 286 | * 287 | * @param mixed $result Response to replace the requested version with. Can be anything 288 | * a normal endpoint can return, or null to not hijack the request. 289 | * @param WP_REST_Server $this Server instance. 290 | * @param WP_REST_Request $request Request used to generate the response. 291 | */ 292 | add_filter( 'rest_pre_dispatch', function( $result, \WP_REST_Server $server, \WP_REST_Request $request ) { 293 | if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) { 294 | return $result; 295 | } 296 | 297 | if ( is_media_site() ) { 298 | return $result; 299 | } 300 | 301 | $media_routes = [ 302 | '/wp/v2/media', 303 | '/regenerate-thumbnails/', 304 | ]; 305 | 306 | foreach ( $media_routes as $route ) { 307 | if ( 0 === strpos( $request->get_route(), $route ) ) { 308 | $request->set_param( 'post', null ); 309 | switch_to_media_site(); 310 | break; 311 | } 312 | } 313 | 314 | return $result; 315 | }, 0, 3 ); 316 | 317 | /** 318 | * Fires after the XML-RPC user has been authenticated, but before the rest of the method logic begins, in order to 319 | * switch to the network media library site when querying media. 320 | * 321 | * @param string $name The method name. 322 | */ 323 | add_action( 'xmlrpc_call', function( string $name ) { 324 | $media_methods = [ 325 | 'metaWeblog.newMediaObject', 326 | 'wp.getMediaItem', 327 | 'wp.getMediaLibrary', 328 | ]; 329 | 330 | if ( in_array( $name, $media_methods, true ) ) { 331 | switch_to_media_site(); 332 | } 333 | }, 0 ); 334 | 335 | /** 336 | * Apply the current site's `upload_files` capability to the network media site. 337 | * 338 | * This grants a user access to the network media site's library, if that user has access to 339 | * the media library of the current site (whichever site the request has been made from). 340 | * 341 | * @param string[] $caps Capabilities for meta capability. 342 | * @param string $cap Capability name. 343 | * @param int $user_id The user ID. 344 | * @param array $args Adds the context to the cap. Typically the object ID. 345 | * 346 | * @return string[] Updated capabilities. 347 | */ 348 | function allow_media_library_access( array $caps, string $cap, int $user_id, array $args ) : array { 349 | if ( get_current_blog_id() !== get_site_id() ) { 350 | return $caps; 351 | } 352 | 353 | if ( ! in_array( $cap, [ 'edit_post', 'upload_files' ], true ) ) { 354 | return $caps; 355 | } 356 | 357 | if ( 'edit_post' === $cap ) { 358 | $content = get_post( $args[0] ); 359 | if ( 'attachment' !== $content->post_type ) { 360 | return $caps; 361 | } 362 | 363 | // Substitute edit_post because the attachment exists only on the network media site. 364 | $cap = get_post_type_object( $content->post_type )->cap->create_posts; 365 | } 366 | 367 | /* 368 | * By the time this function is called, we've already switched context to the network media site. 369 | * Switch back to the original site -- where the initial request came in from. 370 | */ 371 | switch_to_blog( (int) $GLOBALS['current_blog']->blog_id ); 372 | remove_filter( 'map_meta_cap', __NAMESPACE__ . '\allow_media_library_access', 10 ); 373 | 374 | $user_has_permission = user_can( $user_id, $cap ); 375 | 376 | add_filter( 'map_meta_cap', __NAMESPACE__ . '\allow_media_library_access', 10, 4 ); 377 | restore_current_blog(); 378 | 379 | return ( $user_has_permission ? [ 'exist' ] : $caps ); 380 | } 381 | 382 | /** 383 | * Filters 'img' elements in post content to add 'srcset' and 'sizes' attributes. 384 | * 385 | * @see wp_make_content_images_responsive() 386 | * 387 | * @param string $content The raw post content to be filtered. 388 | * @return string Converted content with 'srcset' and 'sizes' attributes added to images. 389 | */ 390 | function make_content_images_responsive( $content ) { 391 | if ( is_media_site() ) { 392 | return $content; 393 | } 394 | 395 | switch_to_media_site(); 396 | 397 | $content = wp_make_content_images_responsive( $content ); 398 | 399 | restore_current_blog(); 400 | 401 | return $content; 402 | } 403 | 404 | remove_filter( 'the_content', 'wp_make_content_images_responsive' ); 405 | add_filter( 'the_content', __NAMESPACE__ . '\make_content_images_responsive' ); 406 | 407 | /** 408 | * A class which encapsulates the filtering of ACF field values. 409 | */ 410 | class ACF_Value_Filter { 411 | 412 | /** 413 | * Stores the value of the field. 414 | * 415 | * @var mixed Field value. 416 | */ 417 | protected $value = null; 418 | 419 | /** 420 | * Sets up the necessary action and filter callbacks. 421 | */ 422 | public function __construct() { 423 | $field_types = [ 424 | 'image', 425 | 'file', 426 | ]; 427 | 428 | foreach ( $field_types as $type ) { 429 | add_filter( "acf/load_value/type={$type}", [ $this, 'filter_acf_attachment_load_value' ], 0, 3 ); 430 | add_filter( "acf/format_value/type={$type}", [ $this, 'filter_acf_attachment_format_value' ], 9999, 3 ); 431 | } 432 | } 433 | 434 | /** 435 | * Fiters the return value when using field retrieval functions in Advanced Custom Fields. 436 | * 437 | * @param mixed $value The field value. 438 | * @param int|string $post_id The post ID for this value. 439 | * @param array $field The field array. 440 | * @return mixed The updated value. 441 | */ 442 | public function filter_acf_attachment_load_value( $value, $post_id, array $field ) { 443 | $image = $value; 444 | 445 | if ( ! is_media_site() && ! is_admin() ) { 446 | switch_to_media_site(); 447 | 448 | switch ( $field['return_format'] ) { 449 | case 'url': 450 | $image = wp_get_attachment_url( $value ); 451 | break; 452 | case 'array': 453 | $image = acf_get_attachment( $value ); 454 | break; 455 | } 456 | 457 | restore_current_blog(); 458 | } 459 | 460 | $this->value = $image; 461 | 462 | return $image; 463 | } 464 | 465 | /** 466 | * Fiters the optionally formatted value when using field retrieval functions in Advanced Custom Fields. 467 | * 468 | * @param mixed $value The field value. 469 | * @param int|string $post_id The post ID for this value. 470 | * @param array $field The field array. 471 | * @return mixed The updated value. 472 | */ 473 | public function filter_acf_attachment_format_value( $value, $post_id, array $field ) { 474 | return $this->value; 475 | } 476 | } 477 | 478 | new ACF_Value_Filter(); 479 | 480 | /** 481 | * A class which encapsulates the rendering of ACF field controls. 482 | */ 483 | class ACF_Field_Rendering { 484 | 485 | /** 486 | * Stored the site switching state between instances of fields. 487 | * 488 | * @var bool Whether the previous field triggered a switch to the central media site. 489 | */ 490 | protected $switched = false; 491 | 492 | /** 493 | * Sets up the necessary action and filter callbacks. 494 | */ 495 | public function __construct() { 496 | add_action( 'acf/render_field', [ $this, 'maybe_restore_current_blog' ], -999 ); 497 | add_action( 'acf/render_field/type=file', [ $this, 'maybe_switch_to_media_site' ], 0 ); 498 | } 499 | 500 | /** 501 | * Switches to the central media site. 502 | */ 503 | public function maybe_switch_to_media_site() { 504 | $this->switched = true; 505 | 506 | switch_to_media_site(); 507 | } 508 | 509 | /** 510 | * Switches back to the current site if the previous field triggered a switch to the central media site. 511 | */ 512 | public function maybe_restore_current_blog() { 513 | if ( ! empty( $this->switched ) ) { 514 | restore_current_blog(); 515 | } 516 | 517 | $this->switched = false; 518 | } 519 | } 520 | 521 | new ACF_Field_Rendering(); 522 | 523 | /** 524 | * A class which handles saving the post's featured image ID. 525 | * 526 | * This handling is required because `wp_insert_post()` checks the validity of the featured image 527 | * ID before saving it to post meta, and deletes it if it's not an image/audio/video. In order to 528 | * override this handling, two consecutive hooks are required to temporarily store the ID of the 529 | * selected featured image and then to save it again after the post has been saved. 530 | */ 531 | class Post_Thumbnail_Saver { 532 | 533 | /** 534 | * Stores the featured image ID for a post ID. 535 | * 536 | * @var int[] Array of featured image IDs keyed by their post ID. 537 | */ 538 | protected $thumbnail_ids = []; 539 | 540 | /** 541 | * Sets up the necessary action and filter callbacks. 542 | */ 543 | public function __construct() { 544 | add_filter( 'wp_insert_post_data', [ $this, 'filter_wp_insert_post_data' ], 10, 2 ); 545 | add_action( 'save_post', [ $this, 'action_save_post' ], 10, 3 ); 546 | } 547 | 548 | /** 549 | * Temporarily stores the ID of the featured image for the given post ID when the post is saved. 550 | * 551 | * @param array $data An array of slashed post data. 552 | * @param array $postarr An array of sanitized, but otherwise unmodified post data. 553 | * @return array An array of slashed post data. 554 | */ 555 | public function filter_wp_insert_post_data( array $data, array $postarr ) : array { 556 | if ( ! empty( $postarr['_thumbnail_id'] ) ) { 557 | $this->thumbnail_ids[ $postarr['ID'] ] = intval( $postarr['_thumbnail_id'] ); 558 | } 559 | 560 | return $data; 561 | } 562 | 563 | /** 564 | * Re-saves the featured image ID for the given post. 565 | * 566 | * @param int $post_id Post ID. 567 | * @param WP_Post $post Post object. 568 | * @param bool $update Whether this is an existing post being updated or not. 569 | */ 570 | public function action_save_post( $post_id, WP_Post $post, bool $update ) { 571 | if ( ! empty( $this->thumbnail_ids[ $post->ID ] ) && ( -1 !== $this->thumbnail_ids[ $post->ID ] ) ) { 572 | update_post_meta( $post->ID, '_thumbnail_id', $this->thumbnail_ids[ $post->ID ] ); 573 | } 574 | } 575 | 576 | } 577 | 578 | new Post_Thumbnail_Saver(); 579 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Network Media Library 2 | 3 | Network Media Library is a plugin for WordPress Multisite which provides a central media library that's shared across all sites on the Multisite network. 4 | 5 | ## Description 6 | 7 | This small plugin transparently shares media from one central media library site to all the other sites on the network. All media that's uploaded gets transparently directed to the central media site, and subsequently made available network-wide. Nothing is copied, cloned, synchronised, or mirrored, so for each file that's uploaded there's only one attachment and one copy of the file. 8 | 9 | ## Minimum Requirements ## 10 | 11 | **PHP:** 7.0 12 | **WordPress:** 4.9 13 | 14 | ## Installation 15 | 16 | The plugin is available as a [Composer package](https://packagist.org/packages/humanmade/network-media-library). 17 | 18 | composer require humanmade/network-media-library 19 | 20 | If you don't use Composer, install the plugin as you would normally. 21 | 22 | The plugin should either be installed as a mu-plugin or network activated. It's a network plugin and therefore cannot be activated on individual sites on the network. 23 | 24 | Site ID `2` is used by default as the central media library. You should configure your media library site ID via the filter hook `network-media-library/site_id`: 25 | 26 | ```php 27 | add_filter( 'network-media-library/site_id', function( $site_id ) { 28 | return 123; 29 | } ); 30 | ``` 31 | 32 | ## Usage 33 | 34 | Use the media library on the sites on your network just as you would normally. All media will be transparently stored on and served from the chosen central media library site. 35 | 36 | Attachments can be deleted only from within the admin area of the central media library. 37 | 38 | ## Compatibility 39 | 40 | Network Media Library works transparently and seamlessly with all built-in WordPress media functionality, including uploading files, cropping images, inserting media into posts, and viewing attachments. Its functionality works with the site icon, site logo, background and header images, featured images, galleries, the audio and image widgets, and regular media management. 41 | 42 | The plugin works with the block editor, the classic editor, the REST API, XML-RPC, and all standard Ajax endpoints for media management. 43 | 44 | Links to media from other sites mostly work, although there are a couple of edge case bugs in WordPress core that need to be fixed (I'll get to these soon). 45 | 46 | Compatibility with third-party plugins is good, but not guaranteed. The following plugins and libraries are explicitly supported by Network Media Library: 47 | 48 | * [Advanced Custom Fields](https://wordpress.org/plugins/advanced-custom-fields/) 49 | * [Regenerate Thumbnails](https://wordpress.org/plugins/regenerate-thumbnails/) 50 | * [WP User Avatars](https://wordpress.org/plugins/wp-user-avatars/) 51 | 52 | The following plugins and libraries have been tested and confirmed as compatible out of the box: 53 | 54 | * [BuddyPress](https://wordpress.org/plugins/buddypress/) 55 | * [Extended CPTs](https://github.com/johnbillion/extended-cpts) 56 | * [Gutenberg](https://wordpress.org/plugins/gutenberg/) 57 | * [Stream](https://wordpress.org/plugins/stream/) 58 | * [User Profile Picture](https://wordpress.org/plugins/metronet-profile-picture/) 59 | 60 | I plan to fully test (and add support if necessary) many other plugins and libraries, including CMB2, Fieldmanager, and many gallery and media management plugins. Stay tuned for updates! 61 | 62 | ## Screenshots 63 | 64 | There are no screenshots to show as Network Media Library operates transparently and introduces no new UI. Simply upload, manage, insert, and use your media as you would normally, and everything will operate through the central media library. 65 | 66 | ## License 67 | 68 | Good news, this plugin is free for everyone! Since it's released under the MIT, you can use it free of charge on your personal or commercial site. 69 | 70 | ## History 71 | 72 | This plugin originally started life as a fork of the [Multisite Global Media plugin](https://github.com/bueltge/multisite-global-media) by Frank Bültge and Dominik Schilling at [Inpsyde](https://inpsyde.com/), but has since diverged entirely and retains little of the original functionality. 73 | 74 | The initial fork of this plugin was made as part of a client project at [Human Made](https://humanmade.com/). We build and manage high-performance WordPress websites for some of the largest publishers in the world. 75 | 76 | Hurrah for open source! 77 | 78 | ## Alternatives 79 | 80 | If the Network Media Library plugin doesn't suit your needs, try these alternatives: 81 | 82 | * [Multisite Global Media](https://github.com/bueltge/multisite-global-media) 83 | * [Network Shared Media](https://wordpress.org/plugins/network-shared-media/) 84 | --------------------------------------------------------------------------------