| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — My WordPress: post-type ownership → folder groups. |
| 4 |
* |
| 5 |
* Custom post types are grouped in the site window by whoever |
| 6 |
* registered them: a plugin's CPTs share one folder named after the |
| 7 |
* plugin, a theme's CPTs share a folder named after the theme, and |
| 8 |
* anything unattributable stays loose at the root alongside Posts and |
| 9 |
* Pages. |
| 10 |
* |
| 11 |
* Attribution reads the registration-time file path captured by |
| 12 |
* `openstation_record_type_registrant()` (`includes/core/payload.php`), |
| 13 |
* which hooks `registered_post_type` and walks the backtrace to the |
| 14 |
* first frame inside an extension directory. |
| 15 |
* |
| 16 |
* Display names are resolved WITHOUT `get_plugins()`. That function |
| 17 |
* lives in `wp-admin/includes/plugin.php`, which Core only loads after |
| 18 |
* `init` has already run — and it scans the whole plugins directory, |
| 19 |
* which is not a cost worth paying on every admin request. Plugin |
| 20 |
* headers are read straight from the main file with `get_file_data()` |
| 21 |
* (first 8 KB only) and themes via `wp_get_theme()`, both of which live |
| 22 |
* in `wp-includes` and are always available. |
| 23 |
* |
| 24 |
* Filterable surface: |
| 25 |
* |
| 26 |
* - `openstation_my_wordpress_post_type_group` |
| 27 |
* - `openstation_my_wordpress_post_type_groups` |
| 28 |
* |
| 29 |
* @package OpenStation |
| 30 |
*/ |
| 31 |
|
| 32 |
defined( 'ABSPATH' ) || exit; |
| 33 |
|
| 34 |
/** |
| 35 |
* Resolve the folder group a post type belongs to. |
| 36 |
* |
| 37 |
* @param string $post_type Post type slug. |
| 38 |
* @return array|null Group descriptor with `id`, `label`, `icon`, and |
| 39 |
* `order` keys, or null when the type should sit |
| 40 |
* loose at the root. |
| 41 |
*/ |
| 42 |
function openstation_my_wordpress_post_type_group( $post_type ) { |
| 43 |
$group = null; |
| 44 |
|
| 45 |
if ( function_exists( 'openstation_type_registrant_file' ) ) { |
| 46 |
$file = openstation_type_registrant_file( (string) $post_type, 'post_type' ); |
| 47 |
if ( null !== $file ) { |
| 48 |
$group = openstation_my_wordpress_group_for_path( $file ); |
| 49 |
} |
| 50 |
} |
| 51 |
|
| 52 |
/** |
| 53 |
* Filter the folder group resolved for a single post type. |
| 54 |
* |
| 55 |
* Return null to pull the type out of its folder and render it |
| 56 |
* loose at the root of the site window, or return a descriptor |
| 57 |
* (`id`, `label`, `icon`, `order`) to override the attribution — |
| 58 |
* useful for a suite of plugins that should share one folder. |
| 59 |
* |
| 60 |
* **Status: Experimental** — the descriptor may gain fields. |
| 61 |
* |
| 62 |
* @param array|null $group Resolved group descriptor, or null. |
| 63 |
* @param string $post_type Post type slug. |
| 64 |
*/ |
| 65 |
$group = apply_filters( 'openstation_my_wordpress_post_type_group', $group, $post_type ); |
| 66 |
|
| 67 |
return is_array( $group ) && ! empty( $group['id'] ) ? $group : null; |
| 68 |
} |
| 69 |
|
| 70 |
/** |
| 71 |
* Map an absolute registration path to a group descriptor. |
| 72 |
* |
| 73 |
* @param string $file Normalized absolute path. |
| 74 |
* @return array|null Group descriptor or null. |
| 75 |
*/ |
| 76 |
function openstation_my_wordpress_group_for_path( $file ) { |
| 77 |
$path = wp_normalize_path( (string) $file ); |
| 78 |
if ( '' === $path ) { |
| 79 |
return null; |
| 80 |
} |
| 81 |
|
| 82 |
// mu-plugins first: on some installs `WPMU_PLUGIN_DIR` sits inside |
| 83 |
// a path that also prefix-matches a theme root, and a mu-plugin is |
| 84 |
// the more specific answer. |
| 85 |
if ( defined( 'WPMU_PLUGIN_DIR' ) ) { |
| 86 |
$mu_dir = trailingslashit( wp_normalize_path( WPMU_PLUGIN_DIR ) ); |
| 87 |
if ( 0 === strpos( $path, $mu_dir ) ) { |
| 88 |
$rel = ltrim( substr( $path, strlen( $mu_dir ) ), '/' ); |
| 89 |
$slug = ( false !== strpos( $rel, '/' ) ) ? strtok( $rel, '/' ) : $rel; |
| 90 |
if ( '' === $slug ) { |
| 91 |
return null; |
| 92 |
} |
| 93 |
$name = openstation_my_wordpress_plugin_header_name( $mu_dir . $rel ); |
| 94 |
return array( |
| 95 |
'id' => 'mu-plugin:' . $slug, |
| 96 |
'label' => '' !== $name ? $name : $slug, |
| 97 |
'icon' => 'dashicons-admin-plugins', |
| 98 |
'order' => 20, |
| 99 |
); |
| 100 |
} |
| 101 |
} |
| 102 |
|
| 103 |
if ( defined( 'WP_PLUGIN_DIR' ) ) { |
| 104 |
$plugins_dir = trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ); |
| 105 |
if ( 0 === strpos( $path, $plugins_dir ) ) { |
| 106 |
$rel = ltrim( substr( $path, strlen( $plugins_dir ) ), '/' ); |
| 107 |
$folder = ( false !== strpos( $rel, '/' ) ) ? strtok( $rel, '/' ) : ''; |
| 108 |
if ( '' === $folder ) { |
| 109 |
// Single-file plugin living directly in `plugins/`. |
| 110 |
$name = openstation_my_wordpress_plugin_header_name( $plugins_dir . $rel ); |
| 111 |
return array( |
| 112 |
'id' => 'plugin:' . $rel, |
| 113 |
'label' => '' !== $name ? $name : $rel, |
| 114 |
'icon' => 'dashicons-admin-plugins', |
| 115 |
'order' => 20, |
| 116 |
); |
| 117 |
} |
| 118 |
return array( |
| 119 |
'id' => 'plugin:' . $folder, |
| 120 |
'label' => openstation_my_wordpress_plugin_folder_name( $folder ), |
| 121 |
'icon' => 'dashicons-admin-plugins', |
| 122 |
'order' => 20, |
| 123 |
); |
| 124 |
} |
| 125 |
} |
| 126 |
|
| 127 |
foreach ( (array) get_theme_roots() as $theme_root ) { |
| 128 |
$root = trailingslashit( wp_normalize_path( get_theme_root( (string) $theme_root ) ) ); |
| 129 |
if ( 0 !== strpos( $path, $root ) ) { |
| 130 |
continue; |
| 131 |
} |
| 132 |
$rel = ltrim( substr( $path, strlen( $root ) ), '/' ); |
| 133 |
$stylesheet = ( false !== strpos( $rel, '/' ) ) ? strtok( $rel, '/' ) : $rel; |
| 134 |
if ( '' === $stylesheet ) { |
| 135 |
continue; |
| 136 |
} |
| 137 |
$theme = wp_get_theme( $stylesheet ); |
| 138 |
$name = $theme->exists() ? (string) $theme->get( 'Name' ) : ''; |
| 139 |
return array( |
| 140 |
'id' => 'theme:' . $stylesheet, |
| 141 |
'label' => '' !== $name ? $name : $stylesheet, |
| 142 |
'icon' => 'dashicons-admin-appearance', |
| 143 |
'order' => 30, |
| 144 |
); |
| 145 |
} |
| 146 |
|
| 147 |
return null; |
| 148 |
} |
| 149 |
|
| 150 |
/** |
| 151 |
* Human-readable name for an active plugin folder. |
| 152 |
* |
| 153 |
* Matches the folder against `wp_get_active_and_valid_plugins()` — which |
| 154 |
* returns absolute paths to each active plugin's main file and is |
| 155 |
* available on every request — then reads the `Plugin Name` header from |
| 156 |
* that file. Falls back to the folder slug when the plugin isn't active |
| 157 |
* or carries no header (a CPT registered from an inactive plugin folder |
| 158 |
* is possible via `require`). |
| 159 |
* |
| 160 |
* @param string $folder Plugin folder name. |
| 161 |
* @return string Display name. |
| 162 |
*/ |
| 163 |
function openstation_my_wordpress_plugin_folder_name( $folder ) { |
| 164 |
$slug = (string) $folder; |
| 165 |
if ( '' === $slug ) { |
| 166 |
return ''; |
| 167 |
} |
| 168 |
|
| 169 |
static $cache = array(); |
| 170 |
if ( isset( $cache[ $slug ] ) ) { |
| 171 |
return $cache[ $slug ]; |
| 172 |
} |
| 173 |
|
| 174 |
$name = ''; |
| 175 |
foreach ( (array) wp_get_active_and_valid_plugins() as $main_file ) { |
| 176 |
$norm = wp_normalize_path( (string) $main_file ); |
| 177 |
$rel = ltrim( str_replace( trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ), '', $norm ), '/' ); |
| 178 |
if ( 0 !== strpos( $rel, $slug . '/' ) ) { |
| 179 |
continue; |
| 180 |
} |
| 181 |
$name = openstation_my_wordpress_plugin_header_name( $norm ); |
| 182 |
break; |
| 183 |
} |
| 184 |
|
| 185 |
$cache[ $slug ] = '' !== $name ? $name : $slug; |
| 186 |
return $cache[ $slug ]; |
| 187 |
} |
| 188 |
|
| 189 |
/** |
| 190 |
* Read the `Plugin Name` header from a plugin file. |
| 191 |
* |
| 192 |
* @param string $file Absolute path to a PHP file. |
| 193 |
* @return string Plugin name, or an empty string. |
| 194 |
*/ |
| 195 |
function openstation_my_wordpress_plugin_header_name( $file ) { |
| 196 |
if ( ! is_string( $file ) || '' === $file || ! is_readable( $file ) ) { |
| 197 |
return ''; |
| 198 |
} |
| 199 |
$data = get_file_data( $file, array( 'Name' => 'Plugin Name' ), 'plugin' ); |
| 200 |
return isset( $data['Name'] ) ? trim( (string) $data['Name'] ) : ''; |
| 201 |
} |
| 202 |
|
| 203 |
/** |
| 204 |
* Collect the distinct groups referenced by a set of entity |
| 205 |
* descriptors, in render order. |
| 206 |
* |
| 207 |
* The bundle renders one folder tile per entry and drills into it via |
| 208 |
* the `group` route. Ordering is `order` ascending, then label, so |
| 209 |
* plugin folders cluster ahead of theme folders. |
| 210 |
* |
| 211 |
* @param array[] $entities Entity descriptors. |
| 212 |
* @return array[] Group descriptors, ordered. |
| 213 |
*/ |
| 214 |
function openstation_my_wordpress_collect_groups( $entities ) { |
| 215 |
$groups = array(); |
| 216 |
|
| 217 |
foreach ( (array) $entities as $entity ) { |
| 218 |
if ( empty( $entity['group'] ) ) { |
| 219 |
continue; |
| 220 |
} |
| 221 |
$id = (string) $entity['group']; |
| 222 |
if ( isset( $groups[ $id ] ) ) { |
| 223 |
continue; |
| 224 |
} |
| 225 |
$groups[ $id ] = array( |
| 226 |
'id' => $id, |
| 227 |
'label' => isset( $entity['groupLabel'] ) ? (string) $entity['groupLabel'] : $id, |
| 228 |
'icon' => isset( $entity['groupIcon'] ) ? (string) $entity['groupIcon'] : 'dashicons-admin-plugins', |
| 229 |
'order' => isset( $entity['groupOrder'] ) ? (int) $entity['groupOrder'] : 20, |
| 230 |
); |
| 231 |
} |
| 232 |
|
| 233 |
$groups = array_values( $groups ); |
| 234 |
usort( |
| 235 |
$groups, |
| 236 |
static function ( $a, $b ) { |
| 237 |
if ( $a['order'] === $b['order'] ) { |
| 238 |
return strnatcasecmp( $a['label'], $b['label'] ); |
| 239 |
} |
| 240 |
return $a['order'] < $b['order'] ? -1 : 1; |
| 241 |
} |
| 242 |
); |
| 243 |
|
| 244 |
/** |
| 245 |
* Filter the folder groups shown at the root of the site window. |
| 246 |
* |
| 247 |
* Each entry declares `id`, `label`, `icon`, and `order`. Removing |
| 248 |
* an entry does not hide its post types — they fall back to |
| 249 |
* rendering loose at the root. To move a type between folders, |
| 250 |
* use `openstation_my_wordpress_post_type_group` instead. |
| 251 |
* |
| 252 |
* **Status: Experimental** — the descriptor may gain fields. |
| 253 |
* |
| 254 |
* @param array[] $groups Ordered group descriptors. |
| 255 |
* @param array[] $entities The entity descriptors they were derived from. |
| 256 |
*/ |
| 257 |
$filtered = apply_filters( 'openstation_my_wordpress_post_type_groups', $groups, $entities ); |
| 258 |
|
| 259 |
return is_array( $filtered ) ? array_values( $filtered ) : $groups; |
| 260 |
} |
| 261 |
|