PluginProbe
Sharing Image / 3.7
Sharing Image v3.7
3.10 trunk 2.0 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.17 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 3.0 3.1 3.2 3.3 All 29 releases
← All changes | classes/class-widget.php +725 -251 2.0.133.7 View file →
@@ -1,7 +1,7 @@
1 1 <?php
2 2 /**
3 - * Widget class
3 + * Display widgets and sidebars on post and term editors screens.
4 4 *
5 5 * @package sharing-image
6 6 * @author Anton Lukin
7 7 */
@@ -7,14 +7,17 @@
7 7 */
8 8
9 9 namespace Sharing_Image;
10 10
11 +use WP_Error;
12 +use Exception;
13 +
11 14 if ( ! defined( 'ABSPATH' ) ) {
12 15 die;
13 16 }
14 17
15 18 /**
16 - * Widget class
19 + * Widget class.
17 20 *
18 21 * @class Widget
19 22 */
20 23 class Widget {
@@ -22,85 +25,148 @@
22 25 * Post meta key to store poster image.
23 26 *
24 27 * @var string
25 28 */
26 - const WIDGET_META = '_sharing_image';
29 + const META_SOURCE = '_sharing_image';
27 30
28 31 /**
29 - * The instance of Settings class.
32 + * Post meta key to store poster image fielset.
30 33 *
31 - * @var instance
34 + * @var string
32 35 */
33 - private $settings;
36 + const META_FIELDSET = '_sharing_image_fieldset';
34 37
35 38 /**
36 - * Widget constructor.
39 + * Common nonce for settings tabs.
40 + *
41 + * @var string
37 42 */
38 - public function __construct() {
39 - $this->settings = new Settings();
40 - }
43 + const WIDGET_NONCE = 'sharing-image-widget';
41 44
42 45 /**
43 46 * Init class actions and filters.
44 47 */
45 - public function init() {
46 - // Init taxonomy term widget.
47 - add_action( 'admin_init', array( $this, 'init_taxonomy_widget' ) );
48 + public static function init() {
49 + add_action( 'init', array( __CLASS__, 'init_post_widget' ) );
50 + add_action( 'init', array( __CLASS__, 'init_post_sidebar' ) );
51 + add_action( 'init', array( __CLASS__, 'init_taxonomy_widget' ) );
48 52
49 - // Init post metabox widget.
50 - add_action( 'admin_init', array( $this, 'init_metabox_widget' ) );
53 + // Generate poster handlers.
54 + add_action( 'wp_ajax_sharing_image_generate', array( __CLASS__, 'handle_ajax_generator' ) );
55 + add_action( 'rest_api_init', array( __CLASS__, 'add_generate_endpoint' ) );
51 56
52 - // Handle metabox AJAX requests.
53 - add_action( 'admin_init', array( $this, 'handle_ajax_requests' ) );
54 -
55 57 // Try to autogenerate poster if it is needed.
56 - add_action( 'wp_insert_post', array( $this, 'autogenerate_poster' ) );
58 + add_action( 'wp_after_insert_post', array( __CLASS__, 'prepare_autogenerated_poster' ), 20, 2 );
57 59 }
58 60
59 61 /**
60 - * Init metabox on post editing page.
62 + * Init post sidebar for gutenberg enabled dashboard.
61 63 */
62 - public function init_metabox_widget() {
63 - /**
64 - * Easy way to hide metabox.
65 - *
66 - * @param bool $hide_metabox Set true to hide metabox.
67 - */
68 - $hide_metabox = apply_filters( 'sharing_image_hide_metabox', false );
64 + public static function init_post_sidebar() {
65 + if ( Config::is_hidden_post_widget() ) {
66 + return;
67 + }
69 68
70 - if ( $hide_metabox ) {
69 + $schema = array(
70 + 'type' => 'object',
71 + 'properties' => array(
72 + 'poster' => array(
73 + 'type' => 'string',
74 + ),
75 + 'width' => array(
76 + 'type' => 'integer',
77 + ),
78 + 'height' => array(
79 + 'type' => 'integer',
80 + ),
81 + 'template' => array(
82 + 'type' => 'string',
83 + ),
84 + 'mode' => array(
85 + 'type' => 'string',
86 + ),
87 + ),
88 + );
89 +
90 + register_meta(
91 + 'post',
92 + self::META_SOURCE,
93 + array(
94 + 'type' => 'object',
95 + 'show_in_rest' => array(
96 + 'schema' => $schema,
97 + ),
98 + 'single' => true,
99 + 'auth_callback' => function () {
100 + return current_user_can( 'edit_posts' );
101 + },
102 + 'sanitize_callback' => array( __CLASS__, 'sanitize_source' ),
103 + )
104 + );
105 +
106 + register_meta(
107 + 'post',
108 + self::META_FIELDSET,
109 + array(
110 + 'type' => 'object',
111 + 'show_in_rest' => array(
112 + 'schema' => array(
113 + 'type' => 'object',
114 + 'properties' => array(),
115 + 'additionalProperties' => array(
116 + 'type' => array( 'string', 'integer' ),
117 + ),
118 + ),
119 + ),
120 + 'single' => true,
121 + 'auth_callback' => function () {
122 + return current_user_can( 'edit_posts' );
123 + },
124 + 'sanitize_callback' => array( __CLASS__, 'sanitize_fieldset' ),
125 + )
126 + );
127 +
128 + // Add required assets and objects.
129 + add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_sidebar_assets' ) );
130 + }
131 +
132 + /**
133 + * Init widget on post editing page.
134 + */
135 + public static function init_post_widget() {
136 + if ( Config::is_hidden_post_widget() ) {
71 137 return;
72 138 }
73 139
74 - add_action( 'add_meta_boxes', array( $this, 'add_metabox' ) );
140 + add_action( 'add_meta_boxes', array( __CLASS__, 'add_metabox' ) );
75 141
76 142 // Handle actions on post save.
77 - add_action( 'save_post', array( $this, 'save_metabox' ), 10, 2 );
143 + add_action( 'save_post', array( __CLASS__, 'save_post_widget' ), 10 );
78 144
79 145 // Add required assets and objects.
80 - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_metabox_assets' ) );
146 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_metabox_assets' ) );
81 147 }
82 148
83 149 /**
84 150 * Init widget on taxonomy term editing page.
85 151 */
86 - public function init_taxonomy_widget() {
152 + public static function init_taxonomy_widget() {
87 153 // Skip if the Premium is not active.
88 - if ( ! $this->settings->is_premium_features() ) {
154 + if ( ! Premium::is_premium_features() ) {
89 155 return;
90 156 }
91 157
92 - $taxonomies = $this->get_widget_taxonomies();
158 + $taxonomies = self::get_widget_taxonomies();
93 159
94 160 foreach ( $taxonomies as $taxonomy ) {
95 161 // Display taxonomy term widget.
96 - add_action( $taxonomy . '_edit_form', array( $this, 'display_widget' ), 10, 2 );
162 + add_action( $taxonomy . '_edit_form', array( __CLASS__, 'display_widget' ), 10, 2 );
97 163
98 164 // Save taxonomy term meta.
99 - add_action( 'edited_' . $taxonomy, array( $this, 'save_taxonomy' ) );
165 + add_action( 'edited_' . $taxonomy, array( __CLASS__, 'save_taxonomy_widget' ) );
100 166
101 167 // Enqueue term assets.
102 - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_taxonomy_assets' ) );
168 + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_taxonomy_assets' ) );
103 169 }
104 170 }
105 171
106 172 /**
@@ -105,15 +171,19 @@
105 171
106 172 /**
107 173 * Add metabox to post editing page.
108 174 */
109 - public function add_metabox() {
175 + public static function add_metabox() {
110 176 add_meta_box(
111 177 'sharing-image-metabox',
112 - esc_html__( 'Sharing Image', 'sharing image' ),
113 - array( $this, 'display_widget' ),
114 - $this->get_metabox_post_types(),
115 - 'side'
178 + esc_html__( 'Sharing Image', 'sharing-image' ),
179 + array( __CLASS__, 'display_widget' ),
180 + self::get_metabox_post_types(),
181 + 'side',
182 + 'high',
183 + array(
184 + '__back_compat_meta_box' => true,
185 + )
116 186 );
117 187 }
118 188
119 189 /**
@@ -120,9 +190,9 @@
120 190 * Add metabox scripts and styles to admin page.
121 191 *
122 192 * @param string $hook_suffix The current admin page.
123 193 */
124 - public function enqueue_metabox_assets( $hook_suffix ) {
194 + public static function enqueue_metabox_assets( $hook_suffix ) {
125 195 if ( ! in_array( $hook_suffix, array( 'post.php', 'post-new.php' ), true ) ) {
126 196 return;
127 197 }
128 198
@@ -127,19 +197,32 @@
127 197 }
128 198
129 199 $screen = get_current_screen();
130 200
131 - if ( ! in_array( $screen->post_type, $this->get_metabox_post_types(), true ) ) {
201 + if ( ! in_array( $screen->post_type, self::get_metabox_post_types(), true ) ) {
132 202 return;
133 203 }
134 204
135 205 $post = get_post();
136 206
207 + if ( use_block_editor_for_post( $post ) ) {
208 + return;
209 + }
210 +
137 211 // Get post meta for current post ID.
138 - $meta = get_post_meta( $post->ID, self::WIDGET_META, true );
212 + $data = self::create_script_object( 'post', $post->ID );
139 213
140 - $this->enqueue_scripts( $this->create_script_object( $meta, 'metabox' ) );
141 - $this->enqueue_styles();
214 + $data['meta'] = array(
215 + 'source' => get_post_meta( $post->ID, self::META_SOURCE, true ),
216 + 'fieldset' => get_post_meta( $post->ID, self::META_FIELDSET, true ),
217 + );
218 +
219 + /**
220 + * Filter widget script object.
221 + *
222 + * @param array $object Array of widget script object.
223 + */
224 + self::enqueue_widget_scripts( $data );
142 225 }
143 226
144 227 /**
145 228 * Add widget scripts and styles to admin page.
@@ -145,9 +228,9 @@
145 228 * Add widget scripts and styles to admin page.
146 229 *
147 230 * @param string $hook_suffix The current admin page.
148 231 */
149 - public function enqueue_taxonomy_assets( $hook_suffix ) {
232 + public static function enqueue_taxonomy_assets( $hook_suffix ) {
150 233 if ( 'term.php' !== $hook_suffix ) {
151 234 return;
152 235 }
153 236
@@ -152,9 +235,9 @@
152 235 }
153 236
154 237 $screen = get_current_screen();
155 238
156 - if ( ! in_array( $screen->taxonomy, $this->get_widget_taxonomies(), true ) ) {
239 + if ( ! in_array( $screen->taxonomy, self::get_widget_taxonomies(), true ) ) {
157 240 return;
158 241 }
159 242
160 243 // phpcs:disable WordPress.Security.NonceVerification
@@ -164,22 +247,79 @@
164 247
165 248 $term_id = absint( $_REQUEST['tag_ID'] );
166 249 // phpcs:enable WordPress.Security.NonceVerification
167 250
168 - // Get term meta for current term ID.
169 - $meta = get_term_meta( $term_id, self::WIDGET_META, true );
251 + $data = self::create_script_object( 'term', $term_id );
170 252
171 - $this->enqueue_scripts( $this->create_script_object( $meta, 'taxonomy' ) );
172 - $this->enqueue_styles();
253 + $data['meta'] = array(
254 + 'source' => get_term_meta( $term_id, self::META_SOURCE, true ),
255 + 'fieldset' => get_term_meta( $term_id, self::META_FIELDSET, true ),
256 + );
257 +
258 + /**
259 + * Filter widget script object.
260 + *
261 + * @param array $object Array of widget script object.
262 + */
263 + self::enqueue_widget_scripts( $data );
173 264 }
174 265
175 266 /**
267 + * Add Gutenberg block scripts and sryles.
268 + *
269 + * @since 3.0
270 + */
271 + public static function enqueue_sidebar_assets() {
272 + if ( ! is_admin() ) {
273 + return;
274 + }
275 +
276 + $screen = get_current_screen();
277 +
278 + if ( ! in_array( $screen->post_type, self::get_metabox_post_types(), true ) ) {
279 + return;
280 + }
281 +
282 + $asset = require SHARING_IMAGE_DIR . 'assets/sidebar/index.asset.php';
283 +
284 + wp_enqueue_script(
285 + 'sharing-image-sidebar',
286 + plugins_url( 'assets/sidebar/index.js', SHARING_IMAGE_FILE ),
287 + $asset['dependencies'],
288 + $asset['version'],
289 + true
290 + );
291 +
292 + wp_enqueue_style(
293 + 'sharing-image-sidebar',
294 + plugins_url( 'assets/sidebar/index.css', SHARING_IMAGE_FILE ),
295 + null,
296 + $asset['version'],
297 + 'all'
298 + );
299 +
300 + // Translations availible only for WP 5.0+.
301 + wp_set_script_translations( 'sharing-image-sidebar', 'sharing-image' );
302 +
303 + $data = array(
304 + 'meta' => array(
305 + 'source' => self::META_SOURCE,
306 + 'fieldset' => self::META_FIELDSET,
307 + ),
308 + 'autogenerate' => Config::get_autogenerate_index(),
309 + 'templates' => Templates::get_templates(),
310 + );
311 +
312 + // Add widget script object.
313 + wp_localize_script( 'sharing-image-sidebar', 'sharingImageSidebar', $data );
314 + }
315 +
316 + /**
176 317 * Save metabox data.
177 318 *
178 - * @param int $post_id Post ID.
179 - * @param WP_Post $post Post object.
319 + * @param int $post_id Post ID.
180 320 */
181 - public function save_metabox( $post_id, $post ) {
321 + public static function save_post_widget( $post_id ) {
182 322 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
183 323 return;
184 324 }
185 325
@@ -195,29 +335,25 @@
195 335 return;
196 336 }
197 337
198 338 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
199 - if ( ! wp_verify_nonce( $_POST['sharing_image_nonce'], basename( __FILE__ ) ) ) {
339 + if ( ! wp_verify_nonce( $_POST['sharing_image_nonce'], self::WIDGET_NONCE ) ) {
200 340 return;
201 341 }
202 342
203 - if ( ! isset( $_POST['sharing_image_picker'] ) ) {
204 - return;
343 + if ( isset( $_POST[ self::META_SOURCE ] ) ) {
344 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
345 + $meta = self::sanitize_source( wp_unslash( $_POST[ self::META_SOURCE ] ) );
346 +
347 + update_post_meta( $post_id, self::META_SOURCE, $meta );
205 348 }
206 349
207 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
208 - $meta = $this->sanitize_picker( wp_unslash( $_POST['sharing_image_picker'] ) );
350 + if ( isset( $_POST[ self::META_FIELDSET ] ) ) {
351 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
352 + $meta = self::sanitize_fieldset( wp_unslash( $_POST[ self::META_FIELDSET ] ) );
209 353
210 - /**
211 - * Filters updated post meta.
212 - *
213 - * @param string $meta Updated post meta.
214 - * @param string $post_id Post ID.
215 - */
216 - $meta = apply_filters( 'sharing_image_update_post_meta', $meta, $post_id );
217 -
218 - // Update post meta.
219 - update_post_meta( $post_id, self::WIDGET_META, $meta );
354 + update_post_meta( $post_id, self::META_FIELDSET, $meta );
355 + }
220 356 }
221 357
222 358 /**
223 359 * Save taxonomy fields.
@@ -223,44 +359,41 @@
223 359 * Save taxonomy fields.
224 360 *
225 361 * @param int $term_id Term ID.
226 362 */
227 - public function save_taxonomy( $term_id ) {
363 + public static function save_taxonomy_widget( $term_id ) {
364 + if ( ! current_user_can( 'edit_term', $term_id ) ) {
365 + return;
366 + }
367 +
228 368 if ( ! isset( $_POST['sharing_image_nonce'] ) ) {
229 369 return;
230 370 }
231 371
232 372 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
233 - if ( ! wp_verify_nonce( $_POST['sharing_image_nonce'], basename( __FILE__ ) ) ) {
373 + if ( ! wp_verify_nonce( $_POST['sharing_image_nonce'], self::WIDGET_NONCE ) ) {
234 374 return;
235 375 }
236 376
237 - if ( ! current_user_can( 'edit_term', $term_id ) ) {
238 - return;
239 - }
377 + if ( isset( $_POST[ self::META_SOURCE ] ) ) {
378 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
379 + $meta = self::sanitize_source( wp_unslash( $_POST[ self::META_SOURCE ] ) );
240 380
241 - if ( ! isset( $_POST['sharing_image_picker'] ) ) {
242 - return;
381 + update_term_meta( $term_id, self::META_SOURCE, $meta );
243 382 }
244 383
245 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
246 - $meta = $this->sanitize_picker( wp_unslash( $_POST['sharing_image_picker'] ) );
384 + if ( isset( $_POST[ self::META_FIELDSET ] ) ) {
385 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
386 + $meta = self::sanitize_fieldset( wp_unslash( $_POST[ self::META_FIELDSET ] ) );
247 387
248 - /**
249 - * Filters term meta on update.
250 - *
251 - * @param string $meta Updated term meta.
252 - * @param string $term_id Term ID.
253 - */
254 - $meta = apply_filters( 'sharing_image_update_term_meta', $meta, $term_id );
255 -
256 - update_term_meta( $term_id, self::WIDGET_META, $meta );
388 + update_term_meta( $term_id, self::META_FIELDSET, $meta );
389 + }
257 390 }
258 391
259 392 /**
260 393 * Display widget.
261 394 */
262 - public function display_widget() {
395 + public static function display_widget() {
263 396 include_once SHARING_IMAGE_DIR . 'templates/widget.php';
264 397
265 398 /**
266 399 * Fires on widget template including.
@@ -268,297 +401,537 @@
268 401 do_action( 'sharing_image_widget' );
269 402 }
270 403
271 404 /**
272 - * Handle widget AJAX requests.
405 + * Create new endpoint for generate button in Gutenberg sidebar.
273 406 */
274 - public function handle_ajax_requests() {
275 - $actions = array(
276 - 'generate' => 'generate_poster',
277 - 'rebuild' => 'rebuild_metabox',
407 + public static function add_generate_endpoint() {
408 + register_rest_route(
409 + 'sharing-image/v1',
410 + '/poster/(?P<id>\d+)',
411 + array(
412 + 'methods' => 'POST',
413 + 'callback' => array( __CLASS__, 'handle_rest_generator' ),
414 + 'permission_callback' => function () {
415 + return current_user_can( 'edit_posts' );
416 + },
417 + )
278 418 );
419 + }
279 420
280 - foreach ( $actions as $key => $method ) {
281 - $action = 'sharing_image_' . $key;
421 + /**
422 + * Sanitize widget source meta.
423 + *
424 + * @param array $source Source meta data.
425 + *
426 + * @return array Sanitized source meta fields.
427 + */
428 + public static function sanitize_source( $source ) {
429 + $sanitized = array();
282 430
283 - if ( method_exists( $this, $method ) ) {
284 - add_action( 'wp_ajax_' . $action, array( $this, $method ) );
431 + if ( isset( $source['poster'] ) ) {
432 + $sanitized['poster'] = sanitize_text_field( $source['poster'] );
433 + }
434 +
435 + if ( ! empty( $source['width'] ) ) {
436 + $sanitized['width'] = absint( $source['width'] );
437 + }
438 +
439 + if ( ! empty( $source['height'] ) ) {
440 + $sanitized['height'] = absint( $source['height'] );
441 + }
442 +
443 + if ( ! empty( $source['template'] ) ) {
444 + $sanitized['template'] = sanitize_key( $source['template'] );
445 + }
446 +
447 + if ( ! empty( $source['mode'] ) ) {
448 + $sanitized['mode'] = sanitize_text_field( $source['mode'] );
449 + }
450 +
451 + /**
452 + * Filters fieldset meta.
453 + *
454 + * @since 3.0
455 + *
456 + * @param array $sanitized List of sanitized fields.
457 + * @param array $source List of fields before sanitization.
458 + */
459 + return apply_filters( 'sharing_image_sanitize_source', $sanitized, $source );
460 + }
461 +
462 + /**
463 + * Sanitize fieldset.
464 + *
465 + * @param array $fieldset Fieldset list.
466 + *
467 + * @return array Sanitized fieldset data.
468 + */
469 + public static function sanitize_fieldset( $fieldset ) {
470 + $sanitized = array();
471 +
472 + // Get list of templates.
473 + $templates = Templates::get_templates();
474 +
475 + foreach ( $templates as $template ) {
476 + if ( empty( $template['layers'] ) ) {
477 + continue;
285 478 }
479 +
480 + foreach ( $template['layers'] as $key => $layer ) {
481 + if ( ! array_key_exists( $key, $fieldset ) ) {
482 + continue;
483 + }
484 +
485 + if ( 'text' === $layer['type'] ) {
486 + $sanitized[ $key ] = sanitize_textarea_field( $fieldset[ $key ] );
487 + }
488 +
489 + if ( 'image' === $layer['type'] ) {
490 + $sanitized[ $key ] = absint( $fieldset[ $key ] );
491 + }
492 + }
286 493 }
494 +
495 + /**
496 + * Filters widget fieldset meta.
497 + *
498 + * @since 3.0
499 + *
500 + * @param array $sanitized Sanitized fieldset list.
501 + * @param array $fieldset Fieldset list before sanitization.
502 + */
503 + return apply_filters( 'sharing_image_sanitize_fieldset', $sanitized, $fieldset );
287 504 }
288 505
289 506 /**
290 - * Generate poster by AJAX request.
507 + * Handle generate button in Gutenberg sidebar.
508 + *
509 + * @param WP_REST_Request $request Request params.
291 510 */
292 - public function generate_poster() {
293 - $check = check_ajax_referer( basename( __FILE__ ), 'sharing_image_nonce', false );
511 + public static function handle_rest_generator( $request ) {
512 + $post_id = $request->get_param( 'id' );
294 513
295 - if ( false === $check ) {
296 - wp_send_json_error( __( 'Invalid security token. Reload the page and retry.', 'sharing-image' ), 403 );
514 + if ( empty( $post_id ) ) {
515 + wp_send_json_error( __( 'Post data is empty.', 'sharing-image' ), 400 );
297 516 }
298 517
299 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
300 - $picker = $this->sanitize_picker( wp_unslash( $_POST['sharing_image_picker'] ) );
518 + $params = $request->get_json_params();
301 519
302 - // Compose new poster data.
303 - $source = ( new Generator() )->compose( $picker );
520 + if ( ! isset( $params['template'] ) ) {
521 + wp_send_json_error( __( 'Incorrect request parameters.', 'sharing-image' ), 400 );
522 + }
304 523
305 - if ( is_wp_error( $source ) ) {
306 - wp_send_json_error( $source->get_error_message(), 400 );
524 + $index = sanitize_key( $params['template'] );
525 +
526 + if ( empty( $params['fieldset'] ) ) {
527 + $params['fieldset'] = array();
307 528 }
308 529
309 - wp_send_json_success( $source );
530 + $fieldset = self::sanitize_fieldset( $params['fieldset'] );
531 +
532 + // Invoke poster generation.
533 + $result = self::generate_poster( $fieldset, $index, $post_id, 'post' );
534 +
535 + if ( is_wp_error( $result ) ) {
536 + wp_send_json_error( $result->get_error_messages(), $result->get_error_data() );
537 + }
538 +
539 + wp_send_json_success( $result );
310 540 }
311 541
312 542 /**
313 - * Send metabox config object to rebuild it on frontend.
543 + * Handle generate button on Classic Editor and taxonomy widget.
314 544 */
315 - public function rebuild_metabox() {
316 - $check = check_ajax_referer( basename( __FILE__ ), 'sharing_image_nonce', false );
545 + public static function handle_ajax_generator() {
546 + $check = check_ajax_referer( self::WIDGET_NONCE, 'sharing_image_nonce', false );
317 547
318 548 if ( false === $check ) {
319 - wp_send_json_error( __( 'Invalid security token. Reload the page and retry.', 'sharing-image' ), 403 );
549 + wp_send_json_error( __( 'Invalid security token. Please reload the page and try again.', 'sharing-image' ), 403 );
320 550 }
321 551
322 - if ( empty( $_POST['post'] ) ) {
323 - return;
552 + if ( empty( $_POST[ self::META_SOURCE ]['template'] ) ) {
553 + wp_send_json_error( __( 'Template ID cannot be empty.', 'sharing-image' ), 400 );
324 554 }
325 555
326 - $post_id = absint( $_POST['post'] );
556 + $index = sanitize_key( $_POST[ self::META_SOURCE ]['template'] );
327 557
328 - // Get post meta for current post ID.
329 - $meta = get_post_meta( $post_id, self::WIDGET_META, true );
558 + $screen_id = 0;
330 559
331 - wp_send_json_success( $this->create_script_object( $meta, 'metabox' ) );
560 + if ( ! empty( $_POST['sharing_image_screen'] ) ) {
561 + $screen_id = absint( wp_unslash( $_POST['sharing_image_screen'] ) );
562 + }
563 +
564 + $context = 'post';
565 +
566 + if ( ! empty( $_POST['sharing_image_context'] ) ) {
567 + $context = sanitize_key( wp_unslash( $_POST['sharing_image_context'] ) );
568 + }
569 +
570 + $fieldset = array();
571 +
572 + if ( ! empty( $_POST[ self::META_FIELDSET ] ) ) {
573 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
574 + $fieldset = self::sanitize_fieldset( wp_unslash( $_POST[ self::META_FIELDSET ] ) );
575 + }
576 +
577 + // Invoke poster generation.
578 + $result = self::generate_poster( $fieldset, $index, $screen_id, $context );
579 +
580 + if ( is_wp_error( $result ) ) {
581 + wp_send_json_error( $result->get_error_messages(), $result->get_error_data() );
582 + }
583 +
584 + wp_send_json_success( $result );
332 585 }
333 586
334 587 /**
335 - * Try to autogenerate poster on post insert or update.
588 + * Prepare poster for autogeneration on post insert or update.
336 589 *
337 - * @param integer $post_id Updated post_id.
590 + * @param int $post_id Updated post_id.
591 + * @param object $post Updated post object.
338 592 */
339 - public function autogenerate_poster( $post_id ) {
340 - /**
341 - * Easy way disable autogeneration.
342 - *
343 - * @since 2.0.12
344 - *
345 - * @param bool $disable_autogeneration Set true to disable autogeneration.
346 - * @param int $post_id Post ID.
347 - */
348 - $disable_autogeneration = apply_filters( 'sharing_image_disable_autogeneration', false, $post_id );
593 + public static function prepare_autogenerated_poster( $post_id, $post ) {
594 + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
595 + return;
596 + }
349 597
350 - if ( $disable_autogeneration ) {
598 + if ( wp_is_post_revision( $post_id ) ) {
351 599 return;
352 600 }
353 601
354 - $status = get_post_status( $post_id );
602 + if ( ( ! defined( 'DOING_CRON' ) || ! DOING_CRON ) && ! current_user_can( 'edit_post', $post_id ) ) {
603 + return;
604 + }
355 605
356 - if ( 'auto-draft' === $status ) {
606 + if ( 'trash' === $post->post_status || 'auto-draft' === $post->post_status ) {
357 607 return;
358 608 }
359 609
360 - $meta = get_post_meta( $post_id, self::WIDGET_META, true );
610 + if ( ! in_array( $post->post_type, self::get_metabox_post_types(), true ) ) {
611 + return;
612 + }
361 613
362 - if ( empty( $meta ) ) {
363 - $meta = array();
614 + $meta = get_post_meta( $post_id, self::META_SOURCE, true );
615 +
616 + if ( ! empty( $meta['mode'] ) && 'manual' === $meta['mode'] ) {
617 + return;
364 618 }
365 619
366 - if ( ! empty( $meta['poster'] ) ) {
620 + self::autogenerate_poster( $post_id );
621 + }
622 +
623 + /**
624 + * Autogenerate poster for post_id and custom index if defined.
625 + * Use this public method to autogenerate poster manually.
626 + *
627 + * @param int $post_id Post ID.
628 + * @param string|null $index Template index.
629 + */
630 + public static function autogenerate_poster( $post_id, $index = null ) {
631 + if ( is_null( $index ) ) {
632 + $index = Config::get_autogenerate_index();
633 + }
634 +
635 + if ( ! Templates::has_template( $index ) ) {
367 636 return;
368 637 }
369 638
370 - $config = $this->settings->get_config();
639 + // Compose fieldset by template index.
640 + $fieldset = self::compose_fields( $index, $post_id );
371 641
372 - if ( ! isset( $config['autogenerate'] ) ) {
373 - return;
642 + /**
643 + * Filters fieldset before autogeneration.
644 + *
645 + * @since 3.0
646 + *
647 + * @param array $fieldset Prepared fieldset data.
648 + * @param string $index Template index.
649 + * @param int $post_id Post ID.
650 + */
651 + $fieldset = apply_filters( 'sharing_image_autogenerated_fieldset', $fieldset, $index, $post_id );
652 +
653 + if ( empty( $fieldset ) ) {
654 + $fieldset = array();
374 655 }
375 656
376 - if ( 'manual' === $config['autogenerate'] ) {
657 + // Invoke poster generation.
658 + $result = self::generate_poster( $fieldset, $index, $post_id, 'post', true );
659 +
660 + if ( is_wp_error( $result ) ) {
377 661 return;
378 662 }
379 663
380 - $meta['template'] = absint( $config['autogenerate'] );
664 + $result['template'] = $index;
381 665
382 - // Generate new poster data using post data.
383 - $source = ( new Generator() )->autogenerate( $meta['template'], $post_id );
384 -
385 666 /**
386 667 * Filters autogenerated poster data.
387 668 *
388 669 * @since 2.0.11
389 670 *
390 - * @param array|false $poster Poster image, width and height data or false if undefined.
391 - * @param integer $post_id Post ID.
671 + * @param array|WP_Erorr $result Poster image, width and height data or WP_Error if undefined.
672 + * @param int $post_id Post ID.
392 673 */
393 - $source = apply_filters( 'sharing_image_autogenerated_poster', $source, $post_id );
674 + $result = apply_filters( 'sharing_image_autogenerated_poster', $result, $post_id );
394 675
395 - if ( empty( $source ) ) {
396 - return;
676 + update_post_meta( $post_id, self::META_SOURCE, $result );
677 + update_post_meta( $post_id, self::META_FIELDSET, $fieldset );
678 + }
679 +
680 + /**
681 + * Generate poster.
682 + * Used in widget, sidebar and for auto-generation.
683 + *
684 + * @param array $fieldset Fieldset data from widget.
685 + * @param int $index Template index from editor.
686 + * @param int $screen_id Post or term ID from admin screen.
687 + * @param string $context Screen ID context field. Can be settings, post or term.
688 + * @param boolean $auto Whether auto-generated poster.
689 + */
690 + private static function generate_poster( $fieldset, $index, $screen_id, $context, $auto = false ) {
691 + $templates = Templates::get_templates();
692 +
693 + if ( ! isset( $templates[ $index ] ) ) {
694 + return new WP_Error( 'generate', esc_html__( 'Incorrect template ID.', 'sharing-image' ), 400 );
397 695 }
398 696
399 - $meta = wp_parse_args( $source, $meta );
697 + // Prepare template editor.
698 + $editor = Generator::prepare_template( $templates[ $index ], $fieldset, $index, $screen_id, $context );
400 699
401 - // Update post meta with new poster link.
402 - update_post_meta( $post_id, self::WIDGET_META, $meta );
700 + if ( ! Generator::check_required( $editor ) ) {
701 + return new WP_Error( 'generate', esc_html__( 'Incorrect template settings.', 'sharing-image' ), 400 );
702 + }
703 +
704 + list( $path, $url ) = Generator::get_upload_file();
705 +
706 + // Generate image and save it to given path.
707 + $poster = Generator::create_poster( $editor, $path );
708 +
709 + if ( is_wp_error( $poster ) ) {
710 + return new WP_Error( 'generate', $poster->get_error_message(), 400 );
711 + }
712 +
713 + $attachment = self::save_attachment( $path, $screen_id, $context );
714 +
715 + if ( is_wp_error( $attachment ) ) {
716 + return new WP_Error( 'attachment', $attachment->get_error_message(), 400 );
717 + }
718 +
719 + $source = array(
720 + 'poster' => $url,
721 + 'width' => $editor['width'],
722 + 'height' => $editor['height'],
723 + 'mode' => 'manual',
724 + );
725 +
726 + if ( ! empty( $auto ) ) {
727 + $source['mode'] = 'auto';
728 + }
729 +
730 + return $source;
403 731 }
404 732
405 733 /**
406 - * Enqueue widget styles.
734 + * Create script object to inject with widget.
735 + *
736 + * @param string $context Widget context. For example: metabox or taxonomy.
737 + * @param int $screen_id Post or taxonomy screen ID.
738 + *
739 + * @return array Filtered widget script object.
407 740 */
408 - private function enqueue_styles() {
409 - wp_enqueue_style(
410 - 'sharing-image-widget',
411 - SHARING_IMAGE_URL . 'assets/styles/widget.css',
412 - array(),
413 - SHARING_IMAGE_VERSION,
414 - 'all'
741 + private static function create_script_object( $context, $screen_id ) {
742 + $object = array(
743 + 'nonce' => wp_create_nonce( self::WIDGET_NONCE ),
744 + 'context' => $context,
745 + 'screen' => $screen_id,
746 +
747 + 'name' => array(
748 + 'source' => self::META_SOURCE,
749 + 'fieldset' => self::META_FIELDSET,
750 + ),
751 + 'links' => array(
752 + 'uploads' => esc_url( admin_url( 'upload.php' ) ),
753 + ),
754 + 'templates' => Templates::get_templates(),
755 + 'autogenerate' => Config::get_autogenerate_index(),
415 756 );
757 +
758 + /**
759 + * Filter widget script object.
760 + *
761 + * @param array $object Array of widget script object.
762 + */
763 + return apply_filters( 'sharing_image_widget_object', $object );
416 764 }
417 765
418 766 /**
419 767 * Enqueue widget scripts.
420 768 *
421 - * @param array $object Widget data object.
769 + * @param array $data Widget data object.
422 770 */
423 - private function enqueue_scripts( $object ) {
771 + private static function enqueue_widget_scripts( $data ) {
772 + $asset = require SHARING_IMAGE_DIR . 'assets/widget/index.asset.php';
773 +
424 774 wp_enqueue_media();
425 775
426 776 wp_enqueue_script(
427 777 'sharing-image-widget',
428 - SHARING_IMAGE_URL . 'assets/scripts/widget.js',
429 - array( 'wp-i18n', 'wp-polyfill-formdata' ),
430 - SHARING_IMAGE_VERSION,
778 + plugins_url( 'assets/widget/index.js', SHARING_IMAGE_FILE ),
779 + $asset['dependencies'],
780 + $asset['version'],
431 781 true
432 782 );
433 783
434 - wp_set_script_translations( 'sharing-image-settings', 'sharing-image' );
784 + wp_enqueue_style(
785 + 'sharing-image-widget',
786 + plugins_url( 'assets/widget/index.css', SHARING_IMAGE_FILE ),
787 + $asset['dependencies'],
788 + $asset['version'],
789 + 'all'
790 + );
435 791
792 + // Translations availible only for WP 5.0+.
793 + wp_set_script_translations( 'sharing-image-widget', 'sharing-image' );
794 +
436 795 // Add widget script object.
437 - wp_localize_script( 'sharing-image-widget', 'sharingImageWidget', $object );
796 + wp_localize_script( 'sharing-image-widget', 'sharingImageWidget', $data );
438 797 }
439 798
440 799 /**
441 - * Sanitize picker widget data before saving.
800 + * Get an array of post types where the metabox is displayed.
442 801 *
443 - * @param array $picker Widget POST data.
802 + * @return array Array of post types.
803 + */
804 + private static function get_metabox_post_types() {
805 + $post_types = get_post_types(
806 + array(
807 + 'public' => true,
808 + )
809 + );
444 810
445 - * @return array Sanitized picker fields.
811 + unset( $post_types['attachment'] );
812 +
813 + /**
814 + * Filter widget post types.
815 + *
816 + * @param array $post_types Array of post types for which the metabox is displayed.
817 + */
818 + return apply_filters( 'sharing_image_metabox_post_types', array_values( $post_types ) );
819 + }
820 +
821 + /**
822 + * Update template with post data for preset fields.
823 + *
824 + * @param string $index Template index.
825 + * @param int $post_id Post id.
826 + * @param array $fieldset Optional. Prepared fieldset to modify.
827 + *
828 + * @return array List of prepared fields.
446 829 */
447 - private function sanitize_picker( $picker ) {
448 - $sanitized = array();
830 + private static function compose_fields( $index, $post_id, $fieldset = array() ) {
831 + $templates = Templates::get_templates();
449 832
450 - if ( isset( $picker['poster'] ) ) {
451 - $sanitized['poster'] = sanitize_text_field( $picker['poster'] );
833 + if ( ! isset( $templates[ $index ] ) ) {
834 + return $fieldset;
452 835 }
453 836
454 - if ( ! empty( $picker['width'] ) ) {
455 - $sanitized['width'] = absint( $picker['width'] );
456 - }
837 + $template = $templates[ $index ];
457 838
458 - if ( ! empty( $picker['height'] ) ) {
459 - $sanitized['height'] = absint( $picker['height'] );
839 + if ( ! isset( $template['layers'] ) ) {
840 + return $fieldset;
460 841 }
461 842
462 - $template = 0;
843 + $layers = $template['layers'];
463 844
464 - if ( isset( $picker['template'] ) ) {
465 - $template = absint( $picker['template'] );
845 + foreach ( $layers as $key => $layer ) {
846 + $field = null;
466 847
467 - if ( count( $this->settings->get_templates() ) <= $template ) {
468 - $template = 0;
848 + if ( empty( $layer['type'] ) ) {
849 + continue;
469 850 }
470 - }
471 851
472 - $sanitized['template'] = $template;
852 + if ( 'text' === $layer['type'] ) {
853 + $field = self::compose_text_presets( $layer, $post_id );
854 + }
473 855
474 - if ( isset( $picker['fieldset'] ) ) {
475 - $fieldset = $picker['fieldset'];
856 + if ( 'image' === $layer['type'] ) {
857 + $field = self::compose_image_presets( $layer, $post_id );
858 + }
476 859
477 - foreach ( $fieldset as $key => $fields ) {
478 - $sanitized['fieldset'][ $key ] = $this->sanitize_fieldset( $fields );
860 + if ( ! empty( $field ) ) {
861 + $fieldset[ $key ] = $field;
479 862 }
480 863 }
481 864
482 - /**
483 - * Filters widget picker sanitized fields.
484 - *
485 - * @param array $sanitized List of sanitized picker fields.
486 - * @param array $picker List of picker fields before sanitization.
487 - */
488 - return apply_filters( 'sharing_image_sanitize_picker', $sanitized, $picker );
865 + return $fieldset;
489 866 }
490 867
491 868 /**
492 - * Sanitize picker widget fieldset list.
869 + * Compose preset fields for text layers.
493 870 *
494 - * @param array $fields Fieldset widget list.
871 + * @param string $layer List of layer options.
872 + * @param int $post_id Post id.
495 873 *
496 - * @return array Sanitized fieldset data.
874 + * @return array|null List of prepared fieldset for text layer.
497 875 */
498 - public function sanitize_fieldset( $fields ) {
499 - $sanitized = array();
876 + private static function compose_text_presets( $layer, $post_id ) {
877 + if ( empty( $layer['preset'] ) || empty( $layer['dynamic'] ) ) {
878 + return null;
879 + }
500 880
501 - if ( ! empty( $fields['captions'] ) && is_array( $fields['captions'] ) ) {
502 - $sanitized['captions'] = array_map( 'sanitize_textarea_field', $fields['captions'] );
881 + $separator = ', ';
882 +
883 + if ( ! empty( $layer['separator'] ) ) {
884 + $separator = $layer['separator'];
503 885 }
504 886
505 - if ( ! empty( $fields['attachment'] ) ) {
506 - $sanitized['attachment'] = absint( $fields['attachment'] );
887 + if ( 'title' === $layer['preset'] ) {
888 + return get_the_title( $post_id );
507 889 }
508 890
509 - return $sanitized;
510 - }
891 + if ( 'excerpt' === $layer['preset'] ) {
892 + $post = get_post( $post_id );
511 893
512 - /**
513 - * Create script object to inject with widget.
514 - *
515 - * @param array $meta Term or Post meta data.
516 - * @param string $context Widget context. For example: metabox or taxonomy.
894 + return $post->post_excerpt;
895 + }
517 896
518 - * @return array Filtered widget script object.
519 - */
520 - private function create_script_object( $meta, $context ) {
521 - $object = array(
522 - 'meta' => $meta,
523 - 'nonce' => wp_create_nonce( basename( __FILE__ ) ),
524 - 'context' => $context,
897 + if ( 'categories' === $layer['preset'] ) {
898 + $categories = get_the_category( $post_id );
525 899
526 - 'links' => array(
527 - 'uploads' => esc_url( admin_url( 'upload.php' ) ),
528 - ),
900 + if ( $categories ) {
901 + return implode( $separator, wp_list_pluck( $categories, 'name' ) );
902 + }
903 + }
529 904
530 - 'templates' => $this->settings->get_templates(),
531 - );
905 + if ( 'tags' === $layer['preset'] ) {
906 + $tags = get_the_tags( $post_id );
532 907
533 - /**
534 - * Filter widget script object.
535 - *
536 - * @param array $object Array of widget script object.
537 - */
538 - return apply_filters( 'sharing_image_widget_object', $object );
908 + if ( $tags ) {
909 + return implode( $separator, wp_list_pluck( $tags, 'name' ) );
910 + }
911 + }
912 +
913 + return null;
539 914 }
540 915
541 916 /**
542 - * Get an array of post types where the metabox is displayed.
917 + * Compose preset fields for image layers.
543 918 *
544 - * @return array Array of post types.
919 + * @param string $layer List of layer options.
920 + * @param int $post_id Post id.
921 + *
922 + * @return array|null List of prepared fieldset for image layer.
545 923 */
546 - private function get_metabox_post_types() {
547 - $post_types = get_post_types(
548 - array(
549 - 'public' => true,
550 - )
551 - );
924 + private static function compose_image_presets( $layer, $post_id ) {
925 + if ( empty( $layer['preset'] ) || empty( $layer['dynamic'] ) ) {
926 + return null;
927 + }
552 928
553 - unset( $post_types['attachment'] );
929 + if ( 'featured' === $layer['preset'] ) {
930 + return absint( get_post_thumbnail_id( $post_id ) );
931 + }
554 932
555 - /**
556 - * Filter widget post types.
557 - *
558 - * @param array $post_types Array of post types for which the metabox is displayed.
559 - */
560 - return apply_filters( 'sharing_image_metabox_post_types', array_values( $post_types ) );
933 + return null;
561 934 }
562 935
563 936 /**
564 937 * Get an array of taxonomeis where the widget is displayed.
@@ -564,9 +937,9 @@
564 937 * Get an array of taxonomeis where the widget is displayed.
565 938 *
566 939 * @return array Array of taxonomies.
567 940 */
568 - private function get_widget_taxonomies() {
941 + private static function get_widget_taxonomies() {
569 942 $taxonomies = get_taxonomies(
570 943 array(
571 944 'public' => true,
572 945 'show_ui' => true,
@@ -578,6 +951,107 @@
578 951 *
579 952 * @param array $taxonomies List of taxonomies to show settings.
580 953 */
581 954 return apply_filters( 'sharing_image_widget_taxonomies', array_values( $taxonomies ) );
955 + }
956 +
957 + /**
958 + * Save attachment to media library.
959 + *
960 + * @param string $path Path to poster image.
961 + * @param int $screen_id Post or taxonomy screen ID.
962 + * @param string $context Widget context. For example: metabox or autogenerate.
963 + *
964 + * @return int|null Attachment ID or null;
965 + */
966 + private static function save_attachment( $path, $screen_id, $context ) {
967 + $config = Config::get_config();
968 +
969 + if ( ! isset( $config['attachment'] ) ) {
970 + return null;
971 + }
972 +
973 + $id = null;
974 +
975 + try {
976 + $name = implode( '-', array( 'sharing-image', $context, $screen_id ) );
977 +
978 + // Try to delete current attachment if exists.
979 + self::delete_attachment( $name );
980 +
981 + $uploads = wp_upload_dir();
982 +
983 + // Get poster filetype.
984 + $filetype = wp_check_filetype( basename( $path ), null );
985 +
986 + $attachment = array(
987 + 'post' => array(
988 + 'guid' => $uploads['url'] . '/' . basename( $path ),
989 + 'post_mime_type' => $filetype['type'],
990 + 'post_name' => $name,
991 + 'post_title' => $name,
992 + 'post_content' => '',
993 + 'post_status' => 'inherit',
994 + ),
995 + 'parent_id' => 0,
996 + );
997 +
998 + if ( 'post' === $context ) {
999 + $attachment['parent_id'] = $screen_id;
1000 + }
1001 +
1002 + /**
1003 + * Filter attachment data before insertion.
1004 + *
1005 + * @param string $attachment Attachment data.
1006 + * @param string $path Path to poster image.
1007 + * @param string $screen_id Post or taxonomy screen ID.
1008 + * @param string $context Widget context. For example: metabox or autogenerate.
1009 + */
1010 + $attachment = apply_filters( 'sharing_image_attachment_data', $attachment, $path, $screen_id, $context );
1011 +
1012 + if ( empty( $attachment['post'] ) ) {
1013 + return null;
1014 + }
1015 +
1016 + if ( ! function_exists( 'wp_crop_image' ) ) {
1017 + include ABSPATH . 'wp-admin/includes/image.php';
1018 + }
1019 +
1020 + $id = wp_insert_attachment( $attachment['post'], $path, $attachment['parent_id'] );
1021 +
1022 + // Get attachment metadata.
1023 + $metadata = wp_generate_attachment_metadata( $id, $path );
1024 +
1025 + wp_update_attachment_metadata( $id, $metadata );
1026 + } catch ( Exception $e ) {
1027 + return new WP_Error( 'attachment', $e->getMessage() );
1028 + }
1029 +
1030 + return $id;
1031 + }
1032 +
1033 + /**
1034 + * Delete attachment by name if exists.
1035 + *
1036 + * @param string $name Name of attachment.
1037 + *
1038 + * @return bool Whether attachment deleted
1039 + */
1040 + private static function delete_attachment( $name ) {
1041 + $posts = get_posts(
1042 + array(
1043 + 'post_type' => 'attachment',
1044 + 'post_name__in' => array( $name ),
1045 + 'update_post_term_cache' => false,
1046 + 'update_post_meta_cache' => false,
1047 + 'posts_per_page' => 1,
1048 + )
1049 + );
1050 +
1051 + if ( empty( $posts[0]->ID ) ) {
1052 + return false;
1053 + }
1054 +
1055 + return wp_delete_attachment( $posts[0]->ID, true );
582 1056 }
583 1057 }