| @@ -1,0 +1,325 @@ | ||
| 1 | +<?php | |
| 2 | +/** | |
| 3 | + * Class for the widget importer. | |
| 4 | + * | |
| 5 | + * Code is mostly from the Widget Importer & Exporter plugin. | |
| 6 | + * | |
| 7 | + * @see https://wordpress.org/plugins/widget-importer-exporter/ | |
| 8 | + * @package ABlocks | |
| 9 | + */ | |
| 10 | + | |
| 11 | +namespace ABlocks\import; | |
| 12 | + | |
| 13 | +use ABlocks\Helper; | |
| 14 | +use WP_Error; | |
| 15 | + | |
| 16 | +class WidgetImporter { | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * Import widgets from WIE or JSON file. | |
| 20 | + * | |
| 21 | + * @param string $widget_import_file_path path to the widget import file. | |
| 22 | + */ | |
| 23 | + public static function import( string $widget_import_file_path ) { | |
| 24 | + $results = array(); | |
| 25 | + | |
| 26 | + // Import widgets and return result. | |
| 27 | + if ( ! empty( $widget_import_file_path ) ) { | |
| 28 | + Helper::emit_sse_message([ | |
| 29 | + 'action' => 'log', | |
| 30 | + 'level' => 'info', | |
| 31 | + 'message' => __( 'Importing widget', 'ablocks' ), | |
| 32 | + ]); | |
| 33 | + $results = self::import_widgets( $widget_import_file_path ); | |
| 34 | + } | |
| 35 | + | |
| 36 | + // Check for errors, else write the results to the log file. | |
| 37 | + if ( is_wp_error( $results ) ) { | |
| 38 | + Helper::emit_sse_message([ | |
| 39 | + 'action' => 'log', | |
| 40 | + 'level' => 'warning', | |
| 41 | + 'message' => $results->get_error_message(), | |
| 42 | + ]); | |
| 43 | + } | |
| 44 | + } | |
| 45 | + | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * Imports widgets from a json file. | |
| 49 | + * | |
| 50 | + * @param string $data_file path to json file with WordPress widget export data. | |
| 51 | + */ | |
| 52 | + private static function import_widgets( string $data_file ) { | |
| 53 | + // Get widgets data from file. | |
| 54 | + $data = self::process_import_file( $data_file ); | |
| 55 | + | |
| 56 | + // Return from this function if there was an error. | |
| 57 | + if ( is_wp_error( $data ) ) { | |
| 58 | + return $data; | |
| 59 | + } | |
| 60 | + | |
| 61 | + // Import the widget data and save the results. | |
| 62 | + return self::import_data( $data ); | |
| 63 | + } | |
| 64 | + | |
| 65 | + /** | |
| 66 | + * Process import file - this parses the widget data and returns it. | |
| 67 | + * | |
| 68 | + * @param string $file path to json file. | |
| 69 | + * | |
| 70 | + * @return object|WP_Error $data decoded JSON string | |
| 71 | + */ | |
| 72 | + private static function process_import_file( string $file ) { | |
| 73 | + // File exists? | |
| 74 | + if ( ! file_exists( $file ) ) { | |
| 75 | + return new WP_Error( | |
| 76 | + 'widget_import_file_not_found', | |
| 77 | + __( 'Error: Widget import file could not be found.', 'ablocks' ) | |
| 78 | + ); | |
| 79 | + } | |
| 80 | + | |
| 81 | + // Get file contents and decode. | |
| 82 | + $data = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents | |
| 83 | + | |
| 84 | + // Return from this function if there was an error. | |
| 85 | + if ( empty( $data ) ) { | |
| 86 | + return new WP_Error( | |
| 87 | + 'widget_import_file_empty', | |
| 88 | + __( 'Error: Widget import file could not be empty.', 'ablocks' ) | |
| 89 | + ); | |
| 90 | + } | |
| 91 | + | |
| 92 | + return json_decode( $data ); | |
| 93 | + } | |
| 94 | + | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * Import widget JSON data | |
| 98 | + * | |
| 99 | + * @param object $data JSON widget data. | |
| 100 | + * | |
| 101 | + * @return WP_Error $results | |
| 102 | + * @global array|WP_Error $wp_registered_sidebars | |
| 103 | + */ | |
| 104 | + private static function import_data( object $data ) { | |
| 105 | + global $wp_registered_sidebars; | |
| 106 | + | |
| 107 | + // Have valid data? If no data or could not decode. | |
| 108 | + if ( empty( $data ) || ! is_object( $data ) ) { | |
| 109 | + return new WP_Error( | |
| 110 | + 'corrupted_widget_import_data', | |
| 111 | + __( 'Error: Widget import data could not be read. Please try a different file.', 'ablocks' ) | |
| 112 | + ); | |
| 113 | + } | |
| 114 | + | |
| 115 | + // Hook before import. | |
| 116 | + do_action( 'ablocks/importer/before_widgets_import' ); | |
| 117 | + $data = apply_filters( 'ablocks/importer/before_widgets_import_data', $data ); | |
| 118 | + | |
| 119 | + // Get all available widgets site supports. | |
| 120 | + $available_widgets = self::available_widgets(); | |
| 121 | + | |
| 122 | + // Get all existing widget instances. | |
| 123 | + $widget_instances = array(); | |
| 124 | + | |
| 125 | + foreach ( $available_widgets as $widget_data ) { | |
| 126 | + $widget_instances[ $widget_data['id_base'] ] = get_option( 'widget_' . $widget_data['id_base'] ); | |
| 127 | + } | |
| 128 | + | |
| 129 | + // Begin results. | |
| 130 | + $results = array(); | |
| 131 | + | |
| 132 | + // Loop import data's sidebars. | |
| 133 | + foreach ( $data as $sidebar_id => $widgets ) { | |
| 134 | + // Skip inactive widgets (should not be in export file). | |
| 135 | + if ( 'wp_inactive_widgets' === $sidebar_id ) { | |
| 136 | + continue; | |
| 137 | + } | |
| 138 | + | |
| 139 | + // Check if sidebar is available on this site. Otherwise, add widgets to inactive, and say so. | |
| 140 | + if ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ) { | |
| 141 | + $sidebar_available = true; | |
| 142 | + $use_sidebar_id = $sidebar_id; | |
| 143 | + $sidebar_message_type = 'success'; | |
| 144 | + $sidebar_message = ''; | |
| 145 | + } else { | |
| 146 | + $sidebar_available = false; | |
| 147 | + $use_sidebar_id = 'wp_inactive_widgets'; // Add to inactive if sidebar does not exist in theme. | |
| 148 | + $sidebar_message_type = 'error'; | |
| 149 | + $sidebar_message = __( 'Sidebar does not exist in theme (moving widget to Inactive)', 'ablocks' ); | |
| 150 | + } | |
| 151 | + | |
| 152 | + // Result for sidebar. | |
| 153 | + $results[ $sidebar_id ]['name'] = ! empty( $wp_registered_sidebars[ $sidebar_id ]['name'] ) ? $wp_registered_sidebars[ $sidebar_id ]['name'] : $sidebar_id; // Sidebar name if theme supports it; otherwise ID. | |
| 154 | + $results[ $sidebar_id ]['message_type'] = $sidebar_message_type; | |
| 155 | + $results[ $sidebar_id ]['message'] = $sidebar_message; | |
| 156 | + $results[ $sidebar_id ]['widgets'] = array(); | |
| 157 | + | |
| 158 | + // Loop widgets. | |
| 159 | + foreach ( $widgets as $widget_instance_id => $widget ) { | |
| 160 | + $fail = false; | |
| 161 | + | |
| 162 | + // Get id_base (remove -# from end) and instance ID number. | |
| 163 | + $id_base = preg_replace( '/-[0-9]+$/', '', $widget_instance_id ); | |
| 164 | + $instance_id_number = str_replace( $id_base . '-', '', $widget_instance_id ); | |
| 165 | + | |
| 166 | + // Does site support this widget? | |
| 167 | + if ( ! isset( $available_widgets[ $id_base ] ) ) { | |
| 168 | + $fail = true; | |
| 169 | + $widget_message_type = 'error'; | |
| 170 | + $widget_message = __( 'Site does not support widget', 'ablocks' ); // Explain why widget not imported. | |
| 171 | + } | |
| 172 | + | |
| 173 | + // Filter to modify settings object before conversion to array and import. | |
| 174 | + // Leave this filter here for backwards compatibility with manipulating objects (before conversion to array below). | |
| 175 | + // Ideally the newer wie_widget_settings_array below will be used instead of this. | |
| 176 | + $widget = apply_filters( 'ablocks/importer/widget_settings', $widget ); // Object. | |
| 177 | + | |
| 178 | + // Convert multidimensional objects to multidimensional arrays. | |
| 179 | + // Some plugins like Jetpack Widget Visibility store settings as multidimensional arrays. | |
| 180 | + // Without this, they are imported as objects and cause fatal error on Widgets page. | |
| 181 | + // If this creates problems for plugins that do actually intend settings in objects then may need to consider other approach: https://wordpress.org/support/topic/problem-with-array-of-arrays. | |
| 182 | + // It is probably much more likely that arrays are used than objects, however. | |
| 183 | + $widget = json_decode( wp_json_encode( $widget ), true ); | |
| 184 | + | |
| 185 | + // Filter to modify settings array. | |
| 186 | + // This is preferred over the older wie_widget_settings filter above. | |
| 187 | + // Do before identical check because changes may make it identical to end result (such as URL replacements). | |
| 188 | + $widget = apply_filters( 'ablocks/importer/widget_settings_array', $widget ); | |
| 189 | + | |
| 190 | + // Does widget with identical settings already exist in same sidebar? | |
| 191 | + if ( ! $fail && isset( $widget_instances[ $id_base ] ) ) { | |
| 192 | + // Get existing widgets in this sidebar. | |
| 193 | + $sidebars_widgets = get_option( 'sidebars_widgets' ); | |
| 194 | + $sidebar_widgets = isset( $sidebars_widgets[ $use_sidebar_id ] ) ? $sidebars_widgets[ $use_sidebar_id ] : array(); // Check Inactive if that's where will go. | |
| 195 | + | |
| 196 | + // Loop widgets with ID base. | |
| 197 | + $single_widget_instances = ! empty( $widget_instances[ $id_base ] ) ? $widget_instances[ $id_base ] : array(); | |
| 198 | + foreach ( $single_widget_instances as $check_id => $check_widget ) { | |
| 199 | + // Is widget in same sidebar and has identical settings? | |
| 200 | + if ( in_array( "$id_base-$check_id", $sidebar_widgets ) && (array) $widget == $check_widget ) { | |
| 201 | + $fail = true; | |
| 202 | + $widget_message_type = 'warning'; | |
| 203 | + $widget_message = __( 'Widget already exists', 'ablocks' ); // Explain why widget not imported. | |
| 204 | + | |
| 205 | + break; | |
| 206 | + } | |
| 207 | + } | |
| 208 | + } | |
| 209 | + | |
| 210 | + // No failure. | |
| 211 | + if ( ! $fail ) { | |
| 212 | + // Add widget instance. | |
| 213 | + $single_widget_instances = get_option( 'widget_' . $id_base ); // All instances for that widget ID base, get fresh every time. | |
| 214 | + $single_widget_instances = ! empty( $single_widget_instances ) ? $single_widget_instances : array( '_multiwidget' => 1 ); // Start fresh if you have to. | |
| 215 | + $single_widget_instances[] = $widget; // Add it. | |
| 216 | + | |
| 217 | + // Get the key it was given. | |
| 218 | + end( $single_widget_instances ); | |
| 219 | + $new_instance_id_number = key( $single_widget_instances ); | |
| 220 | + | |
| 221 | + // If key is 0, make it 1. | |
| 222 | + // When 0, an issue can occur where adding a widget causes data from other widget to load, and the widget doesn't stick (reload wipes it). | |
| 223 | + if ( '0' === strval( $new_instance_id_number ) ) { | |
| 224 | + $new_instance_id_number = 1; | |
| 225 | + $single_widget_instances[ $new_instance_id_number ] = $single_widget_instances[0]; | |
| 226 | + unset( $single_widget_instances[0] ); | |
| 227 | + } | |
| 228 | + | |
| 229 | + // Move _multiwidget to end of array for uniformity. | |
| 230 | + if ( isset( $single_widget_instances['_multiwidget'] ) ) { | |
| 231 | + $multiwidget = $single_widget_instances['_multiwidget']; | |
| 232 | + unset( $single_widget_instances['_multiwidget'] ); | |
| 233 | + $single_widget_instances['_multiwidget'] = $multiwidget; | |
| 234 | + } | |
| 235 | + | |
| 236 | + // Update option with new widget. | |
| 237 | + update_option( 'widget_' . $id_base, $single_widget_instances ); | |
| 238 | + | |
| 239 | + // Assign widget instance to sidebar. | |
| 240 | + $sidebars_widgets = get_option( 'sidebars_widgets' ); // Which sidebars have which widgets, get fresh every time. | |
| 241 | + | |
| 242 | + // Avoid rarely fatal error when the option is an empty string | |
| 243 | + // https://github.com/churchthemes/widget-importer-exporter/pull/11. | |
| 244 | + if ( ! $sidebars_widgets ) { | |
| 245 | + $sidebars_widgets = array(); | |
| 246 | + } | |
| 247 | + | |
| 248 | + $new_instance_id = $id_base . '-' . $new_instance_id_number; // Use ID number from new widget instance. | |
| 249 | + $sidebars_widgets[ $use_sidebar_id ][] = $new_instance_id; // Add new instance to sidebar. | |
| 250 | + update_option( 'sidebars_widgets', $sidebars_widgets ); // Save the amended data. | |
| 251 | + | |
| 252 | + // After widget import action. | |
| 253 | + $after_widget_import = array( | |
| 254 | + 'sidebar' => $use_sidebar_id, | |
| 255 | + 'sidebar_old' => $sidebar_id, | |
| 256 | + 'widget' => $widget, | |
| 257 | + 'widget_type' => $id_base, | |
| 258 | + 'widget_id' => $new_instance_id, | |
| 259 | + 'widget_id_old' => $widget_instance_id, | |
| 260 | + 'widget_id_num' => $new_instance_id_number, | |
| 261 | + 'widget_id_num_old' => $instance_id_number, | |
| 262 | + ); | |
| 263 | + do_action( 'ablocks/importer/after_single_widget_import', $after_widget_import ); | |
| 264 | + | |
| 265 | + // Success message. | |
| 266 | + if ( $sidebar_available ) { | |
| 267 | + $widget_message_type = 'success'; | |
| 268 | + $widget_message = __( 'Imported', 'ablocks' ); | |
| 269 | + } else { | |
| 270 | + $widget_message_type = 'warning'; | |
| 271 | + $widget_message = __( 'Imported to Inactive', 'ablocks' ); | |
| 272 | + } | |
| 273 | + }//end if | |
| 274 | + | |
| 275 | + $widget_name = $available_widgets[ $id_base ]['name'] ?? $id_base; | |
| 276 | + if ( isset( $widget_message_type, $widget_message ) ) { | |
| 277 | + Helper::emit_sse_message([ | |
| 278 | + 'action' => 'log', | |
| 279 | + 'level' => $widget_message_type, | |
| 280 | + 'message' => "$widget_name $widget_message", | |
| 281 | + ]); | |
| 282 | + } | |
| 283 | + | |
| 284 | + // Result for widget instance. | |
| 285 | + $results[ $sidebar_id ]['widgets'][ $widget_instance_id ]['name'] = $widget_name; // Widget name or ID if name not available (not supported by site). | |
| 286 | + $results[ $sidebar_id ]['widgets'][ $widget_instance_id ]['title'] = ! empty( $widget['title'] ) ? $widget['title'] : __( 'No Title', 'ablocks' ); // Show "No Title" if widget instance is untitled. | |
| 287 | + $results[ $sidebar_id ]['widgets'][ $widget_instance_id ]['message_type'] = $widget_message_type; | |
| 288 | + $results[ $sidebar_id ]['widgets'][ $widget_instance_id ]['message'] = $widget_message; | |
| 289 | + | |
| 290 | + }//end foreach | |
| 291 | + }//end foreach | |
| 292 | + | |
| 293 | + // Hook after import. | |
| 294 | + do_action( 'ablocks/importer/after_widgets_import' ); | |
| 295 | + | |
| 296 | + // Return results. | |
| 297 | + return apply_filters( 'ablocks/importer/widget_import_results', $results ); | |
| 298 | + } | |
| 299 | + | |
| 300 | + | |
| 301 | + /** | |
| 302 | + * Available widgets. | |
| 303 | + * | |
| 304 | + * Gather site's widgets into array with ID base, name, etc. | |
| 305 | + * | |
| 306 | + * @global array $wp_registered_widget_controls | |
| 307 | + * @return array $available_widgets, Widget information | |
| 308 | + */ | |
| 309 | + private static function available_widgets(): array { | |
| 310 | + global $wp_registered_widget_controls; | |
| 311 | + | |
| 312 | + $widget_controls = $wp_registered_widget_controls; | |
| 313 | + $available_widgets = array(); | |
| 314 | + | |
| 315 | + foreach ( $widget_controls as $widget ) { | |
| 316 | + if ( ! empty( $widget['id_base'] ) && ! isset( $available_widgets[ $widget['id_base'] ] ) ) { | |
| 317 | + $available_widgets[ $widget['id_base'] ]['id_base'] = $widget['id_base']; | |
| 318 | + $available_widgets[ $widget['id_base'] ]['name'] = $widget['name']; | |
| 319 | + } | |
| 320 | + } | |
| 321 | + | |
| 322 | + return apply_filters( 'ablocks/importer/available_widgets', $available_widgets ); | |
| 323 | + } | |
| 324 | + | |
| 325 | +} | |