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 +731 -237 2.0.83.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,247 +401,359 @@
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 ) );
285 - }
431 + if ( isset( $source['poster'] ) ) {
432 + $sanitized['poster'] = sanitize_text_field( $source['poster'] );
286 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 );
287 460 }
288 461
289 462 /**
290 - * Generate poster by AJAX request.
463 + * Sanitize fieldset.
464 + *
465 + * @param array $fieldset Fieldset list.
466 + *
467 + * @return array Sanitized fieldset data.
291 468 */
292 - public function generate_poster() {
293 - $check = check_ajax_referer( basename( __FILE__ ), 'sharing_image_nonce', false );
469 + public static function sanitize_fieldset( $fieldset ) {
470 + $sanitized = array();
294 471
295 - if ( false === $check ) {
296 - wp_send_json_error( __( 'Invalid security token. Reload the page and retry.', 'sharing-image' ), 403 );
297 - }
472 + // Get list of templates.
473 + $templates = Templates::get_templates();
298 474
299 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
300 - $picker = $this->sanitize_picker( wp_unslash( $_POST['sharing_image_picker'] ) );
475 + foreach ( $templates as $template ) {
476 + if ( empty( $template['layers'] ) ) {
477 + continue;
478 + }
301 479
302 - // Compose new poster.
303 - $poster = ( new Generator() )->compose( $picker );
480 + foreach ( $template['layers'] as $key => $layer ) {
481 + if ( ! array_key_exists( $key, $fieldset ) ) {
482 + continue;
483 + }
304 484
305 - if ( is_wp_error( $poster ) ) {
306 - wp_send_json_error( $poster->get_error_message(), 400 );
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 + }
307 493 }
308 494
309 - wp_send_json_success( $poster );
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 );
310 504 }
311 505
312 506 /**
313 - * Send metabox config object to rebuild it on frontend.
507 + * Handle generate button in Gutenberg sidebar.
508 + *
509 + * @param WP_REST_Request $request Request params.
314 510 */
315 - public function rebuild_metabox() {
316 - $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' );
317 513
318 - if ( false === $check ) {
319 - 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 );
320 516 }
321 517
322 - if ( empty( $_POST['post'] ) ) {
323 - return;
518 + $params = $request->get_json_params();
519 +
520 + if ( ! isset( $params['template'] ) ) {
521 + wp_send_json_error( __( 'Incorrect request parameters.', 'sharing-image' ), 400 );
324 522 }
325 523
326 - $post_id = absint( $_POST['post'] );
524 + $index = sanitize_key( $params['template'] );
327 525
328 - // Get post meta for current post ID.
329 - $meta = get_post_meta( $post_id, self::WIDGET_META, true );
526 + if ( empty( $params['fieldset'] ) ) {
527 + $params['fieldset'] = array();
528 + }
330 529
331 - wp_send_json_success( $this->create_script_object( $meta, 'metabox' ) );
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 );
332 540 }
333 541
334 542 /**
335 - * Generate poster by AJAX request.
336 - *
337 - * @param integer $post_id Desired post_id.
338 - *
339 - * @return string|false Generated poster link.
543 + * Handle generate button on Classic Editor and taxonomy widget.
340 544 */
341 - public function autogenerate_poster( $post_id ) {
342 - $meta = get_post_meta( $post_id, self::WIDGET_META, true );
545 + public static function handle_ajax_generator() {
546 + $check = check_ajax_referer( self::WIDGET_NONCE, 'sharing_image_nonce', false );
343 547
344 - if ( empty( $meta ) ) {
345 - return;
548 + if ( false === $check ) {
549 + wp_send_json_error( __( 'Invalid security token. Please reload the page and try again.', 'sharing-image' ), 403 );
346 550 }
347 551
348 - if ( ! empty( $meta['poster'] ) ) {
349 - return;
552 + if ( empty( $_POST[ self::META_SOURCE ]['template'] ) ) {
553 + wp_send_json_error( __( 'Template ID cannot be empty.', 'sharing-image' ), 400 );
350 554 }
351 555
352 - $config = $this->settings->get_config();
556 + $index = sanitize_key( $_POST[ self::META_SOURCE ]['template'] );
353 557
354 - if ( ! isset( $config['autogenerate'] ) ) {
355 - return;
356 - }
558 + $screen_id = 0;
357 559
358 - if ( 'manual' === $config['autogenerate'] ) {
359 - return;
560 + if ( ! empty( $_POST['sharing_image_screen'] ) ) {
561 + $screen_id = absint( wp_unslash( $_POST['sharing_image_screen'] ) );
360 562 }
361 563
362 - $meta['template'] = absint( $config['autogenerate'] );
564 + $context = 'post';
363 565
364 - // Generate new poster using post data.
365 - $poster = ( new Generator() )->autogenerate( $meta['template'], $post_id );
566 + if ( ! empty( $_POST['sharing_image_context'] ) ) {
567 + $context = sanitize_key( wp_unslash( $_POST['sharing_image_context'] ) );
568 + }
366 569
367 - /**
368 - * Filters autogenerated poster data.
369 - *
370 - * @param array|false $poster Poster image, width and height data or false if undefined.
371 - * @param string $post_id Post ID.
372 - */
373 - $poster = apply_filters( 'sharing_image_autogenerated_poster', $poster, $post_id );
570 + $fieldset = array();
374 571
375 - if ( empty( $poster ) ) {
376 - return;
572 + if ( ! empty( $_POST[ self::META_FIELDSET ] ) ) {
573 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
574 + $fieldset = self::sanitize_fieldset( wp_unslash( $_POST[ self::META_FIELDSET ] ) );
377 575 }
378 576
379 - $meta = wp_parse_args( $poster, $meta );
577 + // Invoke poster generation.
578 + $result = self::generate_poster( $fieldset, $index, $screen_id, $context );
380 579
381 - // Update post meta with new poster link.
382 - update_post_meta( $post_id, self::WIDGET_META, $meta );
383 - }
580 + if ( is_wp_error( $result ) ) {
581 + wp_send_json_error( $result->get_error_messages(), $result->get_error_data() );
582 + }
384 583
385 - /**
386 - * Enqueue widget styles.
387 - */
388 - private function enqueue_styles() {
389 - wp_enqueue_style(
390 - 'sharing-image-widget',
391 - SHARING_IMAGE_URL . 'assets/styles/widget.css',
392 - array(),
393 - SHARING_IMAGE_VERSION,
394 - 'all'
395 - );
584 + wp_send_json_success( $result );
396 585 }
397 586
398 587 /**
399 - * Enqueue widget scripts.
588 + * Prepare poster for autogeneration on post insert or update.
400 589 *
401 - * @param array $object Widget data object.
590 + * @param int $post_id Updated post_id.
591 + * @param object $post Updated post object.
402 592 */
403 - private function enqueue_scripts( $object ) {
404 - wp_enqueue_media();
593 + public static function prepare_autogenerated_poster( $post_id, $post ) {
594 + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
595 + return;
596 + }
405 597
406 - wp_enqueue_script(
407 - 'sharing-image-widget',
408 - SHARING_IMAGE_URL . 'assets/scripts/widget.js',
409 - array( 'wp-i18n', 'wp-polyfill-formdata' ),
410 - SHARING_IMAGE_VERSION,
411 - true
412 - );
598 + if ( wp_is_post_revision( $post_id ) ) {
599 + return;
600 + }
413 601
414 - wp_set_script_translations( 'sharing-image-settings', 'sharing-image' );
602 + if ( ( ! defined( 'DOING_CRON' ) || ! DOING_CRON ) && ! current_user_can( 'edit_post', $post_id ) ) {
603 + return;
604 + }
415 605
416 - // Add widget script object.
417 - wp_localize_script( 'sharing-image-widget', 'sharingImageWidget', $object );
606 + if ( 'trash' === $post->post_status || 'auto-draft' === $post->post_status ) {
607 + return;
608 + }
609 +
610 + if ( ! in_array( $post->post_type, self::get_metabox_post_types(), true ) ) {
611 + return;
612 + }
613 +
614 + $meta = get_post_meta( $post_id, self::META_SOURCE, true );
615 +
616 + if ( ! empty( $meta['mode'] ) && 'manual' === $meta['mode'] ) {
617 + return;
618 + }
619 +
620 + self::autogenerate_poster( $post_id );
418 621 }
419 622
420 623 /**
421 - * Sanitize picker widget data before saving.
624 + * Autogenerate poster for post_id and custom index if defined.
625 + * Use this public method to autogenerate poster manually.
422 626 *
423 - * @param array $picker Widget POST data.
424 -
425 - * @return array Sanitized picker fields.
627 + * @param int $post_id Post ID.
628 + * @param string|null $index Template index.
426 629 */
427 - private function sanitize_picker( $picker ) {
428 - $sanitized = array();
429 -
430 - if ( isset( $picker['poster'] ) ) {
431 - $sanitized['poster'] = sanitize_text_field( $picker['poster'] );
630 + public static function autogenerate_poster( $post_id, $index = null ) {
631 + if ( is_null( $index ) ) {
632 + $index = Config::get_autogenerate_index();
432 633 }
433 634
434 - if ( ! empty( $picker['width'] ) ) {
435 - $sanitized['width'] = absint( $picker['width'] );
635 + if ( ! Templates::has_template( $index ) ) {
636 + return;
436 637 }
437 638
438 - if ( ! empty( $picker['height'] ) ) {
439 - $sanitized['height'] = absint( $picker['height'] );
440 - }
639 + // Compose fieldset by template index.
640 + $fieldset = self::compose_fields( $index, $post_id );
441 641
442 - $template = 0;
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 );
443 652
444 - if ( isset( $picker['template'] ) ) {
445 - $template = absint( $picker['template'] );
446 -
447 - if ( count( $this->settings->get_templates() ) <= $template ) {
448 - $template = 0;
449 - }
653 + if ( empty( $fieldset ) ) {
654 + $fieldset = array();
450 655 }
451 656
452 - $sanitized['template'] = $template;
657 + // Invoke poster generation.
658 + $result = self::generate_poster( $fieldset, $index, $post_id, 'post', true );
453 659
454 - if ( isset( $picker['fieldset'] ) ) {
455 - $fieldset = $picker['fieldset'];
660 + if ( is_wp_error( $result ) ) {
661 + return;
662 + }
456 663
457 - foreach ( $fieldset as $key => $fields ) {
458 - $sanitized['fieldset'][ $key ] = $this->sanitize_fieldset( $fields );
459 - }
460 - }
664 + $result['template'] = $index;
461 665
462 666 /**
463 - * Filters widget picker sanitized fields.
667 + * Filters autogenerated poster data.
464 668 *
465 - * @param array $sanitized List of sanitized picker fields.
466 - * @param array $picker List of picker fields before sanitization.
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.
467 673 */
468 - return apply_filters( 'sharing_image_sanitize_picker', $sanitized, $picker );
674 + $result = apply_filters( 'sharing_image_autogenerated_poster', $result, $post_id );
675 +
676 + update_post_meta( $post_id, self::META_SOURCE, $result );
677 + update_post_meta( $post_id, self::META_FIELDSET, $fieldset );
469 678 }
470 679
471 680 /**
472 - * Sanitize picker widget fieldset list.
681 + * Generate poster.
682 + * Used in widget, sidebar and for auto-generation.
473 683 *
474 - * @param array $fields Fieldset widget list.
475 - *
476 - * @return array Sanitized fieldset data.
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.
477 689 */
478 - public function sanitize_fieldset( $fields ) {
479 - $sanitized = array();
690 + private static function generate_poster( $fieldset, $index, $screen_id, $context, $auto = false ) {
691 + $templates = Templates::get_templates();
480 692
481 - if ( ! empty( $fields['captions'] ) && is_array( $fields['captions'] ) ) {
482 - $sanitized['captions'] = array_map( 'sanitize_textarea_field', $fields['captions'] );
693 + if ( ! isset( $templates[ $index ] ) ) {
694 + return new WP_Error( 'generate', esc_html__( 'Incorrect template ID.', 'sharing-image' ), 400 );
483 695 }
484 696
485 - if ( ! empty( $fields['attachment'] ) ) {
486 - $sanitized['attachment'] = absint( $fields['attachment'] );
697 + // Prepare template editor.
698 + $editor = Generator::prepare_template( $templates[ $index ], $fieldset, $index, $screen_id, $context );
699 +
700 + if ( ! Generator::check_required( $editor ) ) {
701 + return new WP_Error( 'generate', esc_html__( 'Incorrect template settings.', 'sharing-image' ), 400 );
487 702 }
488 703
489 - return $sanitized;
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;
490 731 }
491 732
492 733 /**
493 734 * Create script object to inject with widget.
494 735 *
495 - * @param array $meta Term or Post meta data.
496 - * @param string $context Widget context. For example: metabox or taxonomy.
497 -
736 + * @param string $context Widget context. For example: metabox or taxonomy.
737 + * @param int $screen_id Post or taxonomy screen ID.
738 + *
498 739 * @return array Filtered widget script object.
499 740 */
500 - private function create_script_object( $meta, $context ) {
741 + private static function create_script_object( $context, $screen_id ) {
501 742 $object = array(
502 - 'meta' => $meta,
503 - 'nonce' => wp_create_nonce( basename( __FILE__ ) ),
504 - 'context' => $context,
743 + 'nonce' => wp_create_nonce( self::WIDGET_NONCE ),
744 + 'context' => $context,
745 + 'screen' => $screen_id,
505 746
506 - 'links' => array(
747 + 'name' => array(
748 + 'source' => self::META_SOURCE,
749 + 'fieldset' => self::META_FIELDSET,
750 + ),
751 + 'links' => array(
507 752 'uploads' => esc_url( admin_url( 'upload.php' ) ),
508 753 ),
509 -
510 - 'templates' => $this->settings->get_templates(),
754 + 'templates' => Templates::get_templates(),
755 + 'autogenerate' => Config::get_autogenerate_index(),
511 756 );
512 757
513 758 /**
514 759 * Filter widget script object.
@@ -518,13 +763,46 @@
518 763 return apply_filters( 'sharing_image_widget_object', $object );
519 764 }
520 765
521 766 /**
767 + * Enqueue widget scripts.
768 + *
769 + * @param array $data Widget data object.
770 + */
771 + private static function enqueue_widget_scripts( $data ) {
772 + $asset = require SHARING_IMAGE_DIR . 'assets/widget/index.asset.php';
773 +
774 + wp_enqueue_media();
775 +
776 + wp_enqueue_script(
777 + 'sharing-image-widget',
778 + plugins_url( 'assets/widget/index.js', SHARING_IMAGE_FILE ),
779 + $asset['dependencies'],
780 + $asset['version'],
781 + true
782 + );
783 +
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 + );
791 +
792 + // Translations availible only for WP 5.0+.
793 + wp_set_script_translations( 'sharing-image-widget', 'sharing-image' );
794 +
795 + // Add widget script object.
796 + wp_localize_script( 'sharing-image-widget', 'sharingImageWidget', $data );
797 + }
798 +
799 + /**
522 800 * Get an array of post types where the metabox is displayed.
523 801 *
524 802 * @return array Array of post types.
525 803 */
526 - private function get_metabox_post_types() {
804 + private static function get_metabox_post_types() {
527 805 $post_types = get_post_types(
528 806 array(
529 807 'public' => true,
530 808 )
@@ -540,13 +818,128 @@
540 818 return apply_filters( 'sharing_image_metabox_post_types', array_values( $post_types ) );
541 819 }
542 820
543 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.
829 + */
830 + private static function compose_fields( $index, $post_id, $fieldset = array() ) {
831 + $templates = Templates::get_templates();
832 +
833 + if ( ! isset( $templates[ $index ] ) ) {
834 + return $fieldset;
835 + }
836 +
837 + $template = $templates[ $index ];
838 +
839 + if ( ! isset( $template['layers'] ) ) {
840 + return $fieldset;
841 + }
842 +
843 + $layers = $template['layers'];
844 +
845 + foreach ( $layers as $key => $layer ) {
846 + $field = null;
847 +
848 + if ( empty( $layer['type'] ) ) {
849 + continue;
850 + }
851 +
852 + if ( 'text' === $layer['type'] ) {
853 + $field = self::compose_text_presets( $layer, $post_id );
854 + }
855 +
856 + if ( 'image' === $layer['type'] ) {
857 + $field = self::compose_image_presets( $layer, $post_id );
858 + }
859 +
860 + if ( ! empty( $field ) ) {
861 + $fieldset[ $key ] = $field;
862 + }
863 + }
864 +
865 + return $fieldset;
866 + }
867 +
868 + /**
869 + * Compose preset fields for text layers.
870 + *
871 + * @param string $layer List of layer options.
872 + * @param int $post_id Post id.
873 + *
874 + * @return array|null List of prepared fieldset for text layer.
875 + */
876 + private static function compose_text_presets( $layer, $post_id ) {
877 + if ( empty( $layer['preset'] ) || empty( $layer['dynamic'] ) ) {
878 + return null;
879 + }
880 +
881 + $separator = ', ';
882 +
883 + if ( ! empty( $layer['separator'] ) ) {
884 + $separator = $layer['separator'];
885 + }
886 +
887 + if ( 'title' === $layer['preset'] ) {
888 + return get_the_title( $post_id );
889 + }
890 +
891 + if ( 'excerpt' === $layer['preset'] ) {
892 + $post = get_post( $post_id );
893 +
894 + return $post->post_excerpt;
895 + }
896 +
897 + if ( 'categories' === $layer['preset'] ) {
898 + $categories = get_the_category( $post_id );
899 +
900 + if ( $categories ) {
901 + return implode( $separator, wp_list_pluck( $categories, 'name' ) );
902 + }
903 + }
904 +
905 + if ( 'tags' === $layer['preset'] ) {
906 + $tags = get_the_tags( $post_id );
907 +
908 + if ( $tags ) {
909 + return implode( $separator, wp_list_pluck( $tags, 'name' ) );
910 + }
911 + }
912 +
913 + return null;
914 + }
915 +
916 + /**
917 + * Compose preset fields for image layers.
918 + *
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.
923 + */
924 + private static function compose_image_presets( $layer, $post_id ) {
925 + if ( empty( $layer['preset'] ) || empty( $layer['dynamic'] ) ) {
926 + return null;
927 + }
928 +
929 + if ( 'featured' === $layer['preset'] ) {
930 + return absint( get_post_thumbnail_id( $post_id ) );
931 + }
932 +
933 + return null;
934 + }
935 +
936 + /**
544 937 * Get an array of taxonomeis where the widget is displayed.
545 938 *
546 939 * @return array Array of taxonomies.
547 940 */
548 - private function get_widget_taxonomies() {
941 + private static function get_widget_taxonomies() {
549 942 $taxonomies = get_taxonomies(
550 943 array(
551 944 'public' => true,
552 945 'show_ui' => true,
@@ -558,6 +951,107 @@
558 951 *
559 952 * @param array $taxonomies List of taxonomies to show settings.
560 953 */
561 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 );
562 1056 }
563 1057 }