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 +729 -239 2.0.113.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,281 +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 - $status = get_post_status( $post_id );
593 + public static function prepare_autogenerated_poster( $post_id, $post ) {
594 + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
595 + return;
596 + }
341 597
342 - if ( 'auto-draft' === $status ) {
598 + if ( wp_is_post_revision( $post_id ) ) {
343 599 return;
344 600 }
345 601
346 - $meta = get_post_meta( $post_id, self::WIDGET_META, true );
602 + if ( ( ! defined( 'DOING_CRON' ) || ! DOING_CRON ) && ! current_user_can( 'edit_post', $post_id ) ) {
603 + return;
604 + }
347 605
348 - if ( empty( $meta ) ) {
349 - $meta = array();
606 + if ( 'trash' === $post->post_status || 'auto-draft' === $post->post_status ) {
607 + return;
350 608 }
351 609
352 - if ( ! empty( $meta['poster'] ) ) {
610 + if ( ! in_array( $post->post_type, self::get_metabox_post_types(), true ) ) {
353 611 return;
354 612 }
355 613
356 - $config = $this->settings->get_config();
614 + $meta = get_post_meta( $post_id, self::META_SOURCE, true );
357 615
358 - if ( ! isset( $config['autogenerate'] ) ) {
616 + if ( ! empty( $meta['mode'] ) && 'manual' === $meta['mode'] ) {
359 617 return;
360 618 }
361 619
362 - if ( 'manual' === $config['autogenerate'] ) {
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 ) ) {
363 636 return;
364 637 }
365 638
366 - $meta['template'] = absint( $config['autogenerate'] );
639 + // Compose fieldset by template index.
640 + $fieldset = self::compose_fields( $index, $post_id );
367 641
368 - // Generate new poster data using post data.
369 - $source = ( new Generator() )->autogenerate( $meta['template'], $post_id );
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 );
370 652
653 + if ( empty( $fieldset ) ) {
654 + $fieldset = array();
655 + }
656 +
657 + // Invoke poster generation.
658 + $result = self::generate_poster( $fieldset, $index, $post_id, 'post', true );
659 +
660 + if ( is_wp_error( $result ) ) {
661 + return;
662 + }
663 +
664 + $result['template'] = $index;
665 +
371 666 /**
372 667 * Filters autogenerated poster data.
373 668 *
374 - * @param array|false $poster Poster image, width and height data or false if undefined.
375 - * @param integer $post_id Post ID.
669 + * @since 2.0.11
670 + *
671 + * @param array|WP_Erorr $result Poster image, width and height data or WP_Error if undefined.
672 + * @param int $post_id Post ID.
376 673 */
377 - $source = apply_filters( 'sharing_image_autogenerated_poster', $source, $post_id );
674 + $result = apply_filters( 'sharing_image_autogenerated_poster', $result, $post_id );
378 675
379 - if ( empty( $source ) ) {
380 - 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 );
381 695 }
382 696
383 - $meta = wp_parse_args( $source, $meta );
697 + // Prepare template editor.
698 + $editor = Generator::prepare_template( $templates[ $index ], $fieldset, $index, $screen_id, $context );
384 699
385 - // Update post meta with new poster link.
386 - 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;
387 731 }
388 732
389 733 /**
390 - * 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.
391 740 */
392 - private function enqueue_styles() {
393 - wp_enqueue_style(
394 - 'sharing-image-widget',
395 - SHARING_IMAGE_URL . 'assets/styles/widget.css',
396 - array(),
397 - SHARING_IMAGE_VERSION,
398 - '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(),
399 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 );
400 764 }
401 765
402 766 /**
403 767 * Enqueue widget scripts.
404 768 *
405 - * @param array $object Widget data object.
769 + * @param array $data Widget data object.
406 770 */
407 - 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 +
408 774 wp_enqueue_media();
409 775
410 776 wp_enqueue_script(
411 777 'sharing-image-widget',
412 - SHARING_IMAGE_URL . 'assets/scripts/widget.js',
413 - array( 'wp-i18n', 'wp-polyfill-formdata' ),
414 - SHARING_IMAGE_VERSION,
778 + plugins_url( 'assets/widget/index.js', SHARING_IMAGE_FILE ),
779 + $asset['dependencies'],
780 + $asset['version'],
415 781 true
416 782 );
417 783
418 - 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 + );
419 791
792 + // Translations availible only for WP 5.0+.
793 + wp_set_script_translations( 'sharing-image-widget', 'sharing-image' );
794 +
420 795 // Add widget script object.
421 - wp_localize_script( 'sharing-image-widget', 'sharingImageWidget', $object );
796 + wp_localize_script( 'sharing-image-widget', 'sharingImageWidget', $data );
422 797 }
423 798
424 799 /**
425 - * Sanitize picker widget data before saving.
800 + * Get an array of post types where the metabox is displayed.
426 801 *
427 - * @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 + );
428 810
429 - * @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.
430 829 */
431 - private function sanitize_picker( $picker ) {
432 - $sanitized = array();
830 + private static function compose_fields( $index, $post_id, $fieldset = array() ) {
831 + $templates = Templates::get_templates();
433 832
434 - if ( isset( $picker['poster'] ) ) {
435 - $sanitized['poster'] = sanitize_text_field( $picker['poster'] );
833 + if ( ! isset( $templates[ $index ] ) ) {
834 + return $fieldset;
436 835 }
437 836
438 - if ( ! empty( $picker['width'] ) ) {
439 - $sanitized['width'] = absint( $picker['width'] );
440 - }
837 + $template = $templates[ $index ];
441 838
442 - if ( ! empty( $picker['height'] ) ) {
443 - $sanitized['height'] = absint( $picker['height'] );
839 + if ( ! isset( $template['layers'] ) ) {
840 + return $fieldset;
444 841 }
445 842
446 - $template = 0;
843 + $layers = $template['layers'];
447 844
448 - if ( isset( $picker['template'] ) ) {
449 - $template = absint( $picker['template'] );
845 + foreach ( $layers as $key => $layer ) {
846 + $field = null;
450 847
451 - if ( count( $this->settings->get_templates() ) <= $template ) {
452 - $template = 0;
848 + if ( empty( $layer['type'] ) ) {
849 + continue;
453 850 }
454 - }
455 851
456 - $sanitized['template'] = $template;
852 + if ( 'text' === $layer['type'] ) {
853 + $field = self::compose_text_presets( $layer, $post_id );
854 + }
457 855
458 - if ( isset( $picker['fieldset'] ) ) {
459 - $fieldset = $picker['fieldset'];
856 + if ( 'image' === $layer['type'] ) {
857 + $field = self::compose_image_presets( $layer, $post_id );
858 + }
460 859
461 - foreach ( $fieldset as $key => $fields ) {
462 - $sanitized['fieldset'][ $key ] = $this->sanitize_fieldset( $fields );
860 + if ( ! empty( $field ) ) {
861 + $fieldset[ $key ] = $field;
463 862 }
464 863 }
465 864
466 - /**
467 - * Filters widget picker sanitized fields.
468 - *
469 - * @param array $sanitized List of sanitized picker fields.
470 - * @param array $picker List of picker fields before sanitization.
471 - */
472 - return apply_filters( 'sharing_image_sanitize_picker', $sanitized, $picker );
865 + return $fieldset;
473 866 }
474 867
475 868 /**
476 - * Sanitize picker widget fieldset list.
869 + * Compose preset fields for text layers.
477 870 *
478 - * @param array $fields Fieldset widget list.
871 + * @param string $layer List of layer options.
872 + * @param int $post_id Post id.
479 873 *
480 - * @return array Sanitized fieldset data.
874 + * @return array|null List of prepared fieldset for text layer.
481 875 */
482 - public function sanitize_fieldset( $fields ) {
483 - $sanitized = array();
876 + private static function compose_text_presets( $layer, $post_id ) {
877 + if ( empty( $layer['preset'] ) || empty( $layer['dynamic'] ) ) {
878 + return null;
879 + }
484 880
485 - if ( ! empty( $fields['captions'] ) && is_array( $fields['captions'] ) ) {
486 - $sanitized['captions'] = array_map( 'sanitize_textarea_field', $fields['captions'] );
881 + $separator = ', ';
882 +
883 + if ( ! empty( $layer['separator'] ) ) {
884 + $separator = $layer['separator'];
487 885 }
488 886
489 - if ( ! empty( $fields['attachment'] ) ) {
490 - $sanitized['attachment'] = absint( $fields['attachment'] );
887 + if ( 'title' === $layer['preset'] ) {
888 + return get_the_title( $post_id );
491 889 }
492 890
493 - return $sanitized;
494 - }
891 + if ( 'excerpt' === $layer['preset'] ) {
892 + $post = get_post( $post_id );
495 893
496 - /**
497 - * Create script object to inject with widget.
498 - *
499 - * @param array $meta Term or Post meta data.
500 - * @param string $context Widget context. For example: metabox or taxonomy.
894 + return $post->post_excerpt;
895 + }
501 896
502 - * @return array Filtered widget script object.
503 - */
504 - private function create_script_object( $meta, $context ) {
505 - $object = array(
506 - 'meta' => $meta,
507 - 'nonce' => wp_create_nonce( basename( __FILE__ ) ),
508 - 'context' => $context,
897 + if ( 'categories' === $layer['preset'] ) {
898 + $categories = get_the_category( $post_id );
509 899
510 - 'links' => array(
511 - 'uploads' => esc_url( admin_url( 'upload.php' ) ),
512 - ),
900 + if ( $categories ) {
901 + return implode( $separator, wp_list_pluck( $categories, 'name' ) );
902 + }
903 + }
513 904
514 - 'templates' => $this->settings->get_templates(),
515 - );
905 + if ( 'tags' === $layer['preset'] ) {
906 + $tags = get_the_tags( $post_id );
516 907
517 - /**
518 - * Filter widget script object.
519 - *
520 - * @param array $object Array of widget script object.
521 - */
522 - 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;
523 914 }
524 915
525 916 /**
526 - * Get an array of post types where the metabox is displayed.
917 + * Compose preset fields for image layers.
527 918 *
528 - * @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.
529 923 */
530 - private function get_metabox_post_types() {
531 - $post_types = get_post_types(
532 - array(
533 - 'public' => true,
534 - )
535 - );
924 + private static function compose_image_presets( $layer, $post_id ) {
925 + if ( empty( $layer['preset'] ) || empty( $layer['dynamic'] ) ) {
926 + return null;
927 + }
536 928
537 - unset( $post_types['attachment'] );
929 + if ( 'featured' === $layer['preset'] ) {
930 + return absint( get_post_thumbnail_id( $post_id ) );
931 + }
538 932
539 - /**
540 - * Filter widget post types.
541 - *
542 - * @param array $post_types Array of post types for which the metabox is displayed.
543 - */
544 - return apply_filters( 'sharing_image_metabox_post_types', array_values( $post_types ) );
933 + return null;
545 934 }
546 935
547 936 /**
548 937 * Get an array of taxonomeis where the widget is displayed.
@@ -548,9 +937,9 @@
548 937 * Get an array of taxonomeis where the widget is displayed.
549 938 *
550 939 * @return array Array of taxonomies.
551 940 */
552 - private function get_widget_taxonomies() {
941 + private static function get_widget_taxonomies() {
553 942 $taxonomies = get_taxonomies(
554 943 array(
555 944 'public' => true,
556 945 'show_ui' => true,
@@ -562,6 +951,107 @@
562 951 *
563 952 * @param array $taxonomies List of taxonomies to show settings.
564 953 */
565 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 );
566 1056 }
567 1057 }