├── MetaLocation └── Blog.php ├── acf-multisite-options.php ├── composer.json ├── index.php └── readme.md /MetaLocation/Blog.php: -------------------------------------------------------------------------------- 1 | _blog_specific_field_types = apply_filters('acf/multisite/blog_specific_field_types', [ 83 | 'post', 'relationship', 'image', 'file' 84 | ]); 85 | 86 | $this->_current_site = get_current_site(); 87 | $acf_admin_options_page = $this->get_acf_admin_options_page(); 88 | 89 | if( is_admin() && $acf_admin_options_page ){ 90 | 91 | // Run the ACF Options admin_menu function on network menu 92 | add_action( 'network_admin_menu', [$acf_admin_options_page, 'admin_menu'], 99, 0 ); 93 | 94 | // Filter out pages by "network" attribute when loading the pages 95 | // for the admin_menu depending on the context 96 | add_action( 'admin_menu', [$this, 'before_admin_menu'], 1 ); 97 | add_action( 'network_admin_menu', [$this, 'before_admin_menu'], 1 ); 98 | add_filter( 'acf/get_options_pages', [$this, 'filter_options_pages'] ); 99 | 100 | } 101 | 102 | add_filter('acf/validate_options_page', [$this, 'capture_network_pages'], 3000 ); 103 | add_filter('acf/pre_load_post_id', [$this, 'convert_post_id'], 10, 2); 104 | 105 | // Wrap some fields with "switch_to_blog()" calls to retrieve images/ posts 106 | add_filter( 'acf/format_value', [$this, 'format_value_start'], 1, 3 ); 107 | add_filter( 'acf/format_value', [$this, 'format_value_end'], 999, 3 ); 108 | 109 | // lets add an option for the blog to specify for these 110 | add_action('acf/render_field_settings', [$this, 'add_blog_setting'], 10, 1); 111 | 112 | } 113 | 114 | public function capture_network_pages( $page ) 115 | { 116 | if( isset($page['network']) && $page['network'] ){ 117 | $this->_network_pages[$page['post_id']] = $page; 118 | } 119 | return $page; 120 | } 121 | 122 | public function convert_post_id( $preload, $post_id ) 123 | { 124 | if( is_numeric($post_id) ){ 125 | return $preload; 126 | } 127 | 128 | if( $post_id && is_string($post_id) && isset( $this->_network_pages[$post_id]) ){ 129 | return 'site_'.$this->_current_site->id; 130 | } 131 | return $preload; 132 | } 133 | 134 | /** 135 | * Sneaky way to get the instantiated "$acf_admin_options_page" object 136 | * as it does not have a global reference. 137 | * 138 | * @return mixed Returns either the page array or false 139 | */ 140 | public function get_acf_admin_options_page() 141 | { 142 | static $acf_admin_options_page = null; 143 | if( isset( $acf_admin_options_page ) ){ 144 | return $acf_admin_options_page; 145 | } 146 | $acf_admin_options_page = false; 147 | global $wp_filter; 148 | if( empty( $wp_filter['admin_menu'] ) || empty( $wp_filter['admin_menu'][99] ) ){ 149 | return $acf_admin_options_page; 150 | } 151 | foreach( $wp_filter['admin_menu'][99] as $hook ){ 152 | if( is_array( $hook['function'] ) && get_class( $hook['function'][0] ) === 'acf_admin_options_page' ){ 153 | $acf_admin_options_page = $hook['function'][0]; 154 | break; 155 | } 156 | } 157 | return $acf_admin_options_page; 158 | } 159 | 160 | /** 161 | * Enabling page filtering for the acf_get_options_pages function 162 | * when we are in the admin menu 163 | * @return void 164 | */ 165 | public function before_admin_menu() 166 | { 167 | $this->_filter_options_pages = true; 168 | } 169 | 170 | /** 171 | * Filter the options pages depending on the context, network or not network. 172 | * 173 | * @param array $pages ACF options pages 174 | * @return array The filtered pages. 175 | */ 176 | public function filter_options_pages( array $pages ) 177 | { 178 | if( !$this->_filter_options_pages ){ 179 | return $pages; 180 | } 181 | 182 | $this->_filter_options_pages = false; 183 | 184 | return array_filter( $pages, function( $page ){ 185 | return is_network_admin() ? !empty($page['network']) : empty( $page['network'] ); 186 | }); 187 | } 188 | 189 | /** 190 | * Retrieve an options page by its "post_id" attribute 191 | * 192 | * There could be multiple pages with the same post_id, 193 | * but we require that network pages either share a post_id 194 | * or each have their own. 195 | * 196 | * @param string $post_id the ACF post_id 197 | * @return mixed the options page or false if none found 198 | */ 199 | public function get_options_page_by_post_id( $post_id ) 200 | { 201 | $pages = acf_get_options_pages(); 202 | foreach( $pages as $page ){ 203 | if( !empty( $page['post_id'] ) && $page['post_id'] === $post_id ){ 204 | return $page; 205 | } 206 | } 207 | return null; 208 | } 209 | 210 | public function format_value_start( $value, $post_id, $field ) 211 | { 212 | if( !empty($field['mulitisite_blog_id']) ){ 213 | switch_to_blog( $field['mulitisite_blog_id'] ); 214 | } 215 | return $value; 216 | } 217 | 218 | public function format_value_end( $value, $post_id, $field ) 219 | { 220 | if( !empty($field['mulitisite_blog_id']) ){ 221 | restore_current_blog(); 222 | } 223 | return $value; 224 | } 225 | 226 | public function add_blog_setting($field) 227 | { 228 | if( !in_array( $field['type'], $this->_blog_specific_field_types) ){ 229 | return; 230 | } 231 | 232 | // otherwise, lets add that setting... 233 | $sites = get_sites(); 234 | $choices = []; 235 | foreach( $sites as $site ){ 236 | $choices[$site->blog_id] = $site->domain.$site->path; 237 | } 238 | 239 | acf_render_field_setting( $field, array( 240 | 'label' => __('Site'), 241 | 'instructions' => '', 242 | 'name' => 'multisite_blog_id', 243 | 'type' => 'select', 244 | 'allow_null' => false, 245 | 'ui' => 1, 246 | 'default' => get_main_site_id(), 247 | 'choices' => $choices 248 | ), true); 249 | } 250 | 251 | } 252 | 253 | // Instantiate our plugin 254 | Plugin::getInstance(); 255 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "owlwatch/acf-multisite-options", 3 | "version": "1.0.0", 4 | "description": "ACF Multisite Options", 5 | "homepage": "https://github.com/owlwatch/acf-multisite-options", 6 | "type": "wordpress-plugin", 7 | "authors": [ 8 | { 9 | "name": "Mark Fabrizio", 10 | "email": "fabrizim@owlwatch.com", 11 | "homepage": "http://owlwatch.com/", 12 | "role": "Developer" 13 | } 14 | ], 15 | "license": [ 16 | "GPL-2.0-or-later" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | true, 32 | 'post_id' => 'acf_network_options', 33 | 'page_title' => 'Network Options', 34 | 'menu_title' => 'Network Options' 35 | ]); 36 | 37 | acf_add_options_page([ 38 | 'network' => true, 39 | 'post_id' => 'acf_network_options2', 40 | 'page_title' => 'Network Options 2', 41 | 'menu_title' => 'Network Options 2', 42 | 'parent_slug' => 'settings.php' 43 | ]); 44 | ``` 45 | 46 | ## Frequently Asked Questions 47 | 48 | ### Where are the values stored? 49 | 50 | The values are stored in the `sitemeta` table, allowing for access 51 | across all sites in the network. 52 | 53 | ## Notes 54 | 55 | Field groups for the multisite options pages should be saved and loaded 56 | to a Network Activated plugin via [https://www.advancedcustomfields.com/resources/local-json/](ACF local JSON). 57 | 58 | Each network options page should have a unique post_id. Fields names should be 59 | unique across all network pages as well. 60 | 61 | `get_fields` will return all fields for the network, not just for the requested network post_id. 62 | 63 | ## Changelog 64 | 65 | ### 2.0.2 66 | * Implement ACF\Meta\MetaLocation for Site for ACF 6.4+ compatability 67 | 68 | ### 2.0.1 69 | * Did not document and don't remember :) 70 | 71 | ### 2.0 72 | * Reduce acf code duplication by using the "site_" rather than imitating 73 | an option page 74 | 75 | ### 1.0 76 | * Initial Release --------------------------------------------------------------------------------