@@ -64,6 +64,7 @@ public function __construct() {
6464
6565 // Getting products list
6666 add_action ( 'wp_ajax_ppom_get_products ' , array ( $ this , 'get_products ' ) );
67+ add_action ( 'wp_ajax_ppom_search_products ' , array ( $ this , 'search_products ' ) );
6768 add_action ( 'wp_ajax_ppom_attach_ppoms ' , array ( $ this , 'ppom_attach_ppoms ' ) );
6869
6970 // Adding setting tab in WooCommerce
@@ -329,10 +330,11 @@ public function get_products() {
329330 ->set_select (
330331 array_merge (
331332 array (
332- 'label ' => __ ( 'Choose Products: ' , 'woocommerce-product-addon ' ),
333- 'name ' => 'ppom-attach-to-products[] ' ,
334- 'multiple ' => true ,
335- 'is_used ' => true ,
333+ 'label ' => __ ( 'Choose Products: ' , 'woocommerce-product-addon ' ),
334+ 'name ' => 'ppom-attach-to-products[] ' ,
335+ 'multiple ' => true ,
336+ 'is_used ' => true ,
337+ 'render_empty_option ' => false ,
336338 ),
337339 $ this ->get_wc_products ( $ ppom_id )
338340 )
@@ -418,67 +420,162 @@ function ( $html_content ) use ( $popup_components ) {
418420 }
419421
420422 /**
421- * Returns product selector options for the attach popup.
422- *
423- * Marks which products already reference the current PPOM ID through
424- * {@see PPOM_PRODUCT_META_KEY}.
425- *
426- * @param int $ppom_id PPOM field-group ID being attached.
423+ * Returns paginated product matches for the attach modal Select2 control.
427424 *
428- * @return array
425+ * @return void
429426 */
430- public function get_wc_products ( $ ppom_id ) {
431- $ result = array (
432- 'options ' => array (),
433- 'is_used ' => true ,
427+ public function search_products () {
428+ $ nonce = isset ( $ _GET ['ppom_attached_nonce ' ] ) ? sanitize_text_field ( wp_unslash ( $ _GET ['ppom_attached_nonce ' ] ) ) : '' ;
429+
430+ if ( ! wp_verify_nonce ( $ nonce , 'ppom_attached_nonce_action ' ) || ! ppom_security_role () ) {
431+ wp_send_json_error (
432+ array (
433+ 'message ' => __ ( 'Sorry, you are not allowed to perform this action please try again ' , 'woocommerce-product-addon ' ),
434+ ),
435+ 403
436+ );
437+ }
438+
439+ $ search_term = '' ;
440+ if ( isset ( $ _GET ['q ' ] ) ) {
441+ $ search_term = sanitize_text_field ( wp_unslash ( $ _GET ['q ' ] ) );
442+ } elseif ( isset ( $ _GET ['term ' ] ) ) {
443+ $ search_term = sanitize_text_field ( wp_unslash ( $ _GET ['term ' ] ) );
444+ }
445+
446+ $ page = isset ( $ _GET ['page ' ] ) ? max ( 1 , absint ( $ _GET ['page ' ] ) ) : 1 ;
447+ $ per_page = 20 ;
448+ $ query_args = array (
449+ 'post_type ' => 'product ' ,
450+ 'post_status ' => 'publish ' ,
451+ 'posts_per_page ' => $ per_page ,
452+ 'paged ' => $ page ,
453+ 'orderby ' => 'title ' ,
454+ 'order ' => 'ASC ' ,
455+ 'fields ' => 'ids ' ,
456+ 'no_found_rows ' => false ,
457+ 'update_post_meta_cache ' => false ,
458+ 'update_post_term_cache ' => false ,
434459 );
435460
436- if ( 'valid ' === apply_filters ( 'product_ppom_license_status ' , '' ) ) {
437- $ result ['options ' ][] = array (
438- 'value ' => '-1 ' ,
439- 'selected ' => false ,
440- 'label ' => __ ( 'Select a product ' , 'woocommerce-product-addon ' ),
441- 'disabled ' => true ,
442- );
461+ if ( '' !== $ search_term ) {
462+ $ query_args ['s ' ] = $ search_term ;
443463 }
444464
445- $ query = new WP_Query (
465+ $ query = new WP_Query ( $ query_args );
466+ $ results = array_map (
467+ function ( $ product_id ) {
468+ $ product_id = $ product_id instanceof WP_Post ? (int ) $ product_id ->ID : absint ( $ product_id );
469+
470+ return array (
471+ 'id ' => (string ) $ product_id ,
472+ 'text ' => get_the_title ( $ product_id ),
473+ );
474+ },
475+ $ query ->posts
476+ );
477+
478+ wp_send_json (
446479 array (
447- 'post_type ' => 'product ' ,
448- 'posts_per_page ' => -1 , // Get all products
449- 'post_status ' => 'publish ' ,
480+ 'results ' => $ results ,
481+ 'pagination ' => array (
482+ 'more ' => $ page < (int ) $ query ->max_num_pages ,
483+ ),
450484 )
451485 );
486+ }
452487
453- if ( ! $ query ->have_posts () ) {
454- return $ result ;
488+ /**
489+ * Returns product IDs currently attached to a PPOM field group.
490+ *
491+ * Supports both the current serialized-array storage and the older scalar
492+ * storage used by {@see PPOM_PRODUCT_META_KEY}.
493+ *
494+ * @param int $ppom_id PPOM field-group ID.
495+ * @return int[]
496+ */
497+ public static function get_attached_product_ids ( $ ppom_id ) {
498+ $ ppom_id = absint ( $ ppom_id );
499+ if ( 0 === $ ppom_id ) {
500+ return array ();
455501 }
456502
457- while ( $ query ->have_posts () ) {
458- $ query ->the_post ();
503+ $ ppom_id_string = (string ) $ ppom_id ;
504+ $ product_ids = get_posts (
505+ array (
506+ 'post_type ' => 'product ' ,
507+ 'post_status ' => 'publish ' ,
508+ // Intentionally load all matched attachments, not the full catalog.
509+ // The modal must pre-render every currently attached product as a
510+ // selected option so Select2 shows the existing state correctly.
511+ 'posts_per_page ' => -1 ,
512+ 'fields ' => 'ids ' ,
513+ 'orderby ' => 'title ' ,
514+ 'order ' => 'ASC ' ,
515+ 'no_found_rows ' => true ,
516+ 'update_post_meta_cache ' => false ,
517+ 'update_post_term_cache ' => false ,
518+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Attachments are stored in post meta and this helper needs every selected product ID for modal preloading.
519+ 'meta_query ' => array (
520+ 'relation ' => 'OR ' ,
521+ array (
522+ 'key ' => PPOM_PRODUCT_META_KEY ,
523+ 'value ' => $ ppom_id_string ,
524+ 'compare ' => '= ' ,
525+ ),
526+ array (
527+ 'key ' => PPOM_PRODUCT_META_KEY ,
528+ 'value ' => sprintf ( 'i:%d; ' , $ ppom_id ),
529+ 'compare ' => 'LIKE ' ,
530+ ),
531+ array (
532+ 'key ' => PPOM_PRODUCT_META_KEY ,
533+ 'value ' => sprintf ( 's:%d:"%s"; ' , strlen ( $ ppom_id_string ), $ ppom_id_string ),
534+ 'compare ' => 'LIKE ' ,
535+ ),
536+ ),
537+ )
538+ );
459539
460- $ is_used = false ;
461- $ product_id = get_the_ID ();
462- $ attached_meta = get_post_meta ( $ product_id , PPOM_PRODUCT_META_KEY , true );
540+ $ verified = array ();
541+ foreach ( $ product_ids as $ candidate_id ) {
542+ $ candidate_id = absint ( $ candidate_id );
543+ $ attached_meta = get_post_meta ( $ candidate_id , PPOM_PRODUCT_META_KEY , true );
463544
464- if ( is_array ( $ attached_meta ) ) {
465- $ is_used = in_array ( $ ppom_id , $ attached_meta ) ;
466- } elseif ( is_numeric ( $ attached_meta ) ) {
467- $ is_used = $ product_id === $ attached_meta ; // Note: Legacy format.
545+ if ( is_array ( $ attached_meta ) && in_array ( $ ppom_id , array_map ( ' absint ' , $ attached_meta ), true ) ) {
546+ $ verified [] = $ candidate_id ;
547+ } elseif ( is_numeric ( $ attached_meta ) && absint ( $ attached_meta ) === $ ppom_id ) {
548+ $ verified [] = $ candidate_id ;
468549 }
550+ }
469551
470- if ( $ is_used ) {
471- $ result ['is_used ' ] = true ;
472- }
552+ return array_values ( array_unique ( $ verified ) );
553+ }
473554
555+ /**
556+ * Returns product selector options for the attach popup.
557+ *
558+ * Marks which products already reference the current PPOM ID through
559+ * {@see PPOM_PRODUCT_META_KEY}.
560+ *
561+ * @param int $ppom_id PPOM field-group ID being attached.
562+ *
563+ * @return array
564+ */
565+ public function get_wc_products ( $ ppom_id ) {
566+ $ result = array (
567+ 'options ' => array (),
568+ 'is_used ' => true ,
569+ );
570+
571+ foreach ( self ::get_attached_product_ids ( $ ppom_id ) as $ product_id ) {
474572 $ result ['options ' ][] = array (
475- 'value ' => $ product_id ,
476- 'selected ' => $ is_used ,
477- 'label ' => get_the_title (),
573+ 'value ' => ( string ) $ product_id ,
574+ 'selected ' => true ,
575+ 'label ' => get_the_title ( $ product_id ),
478576 );
479577 }
480578
481- wp_reset_postdata ();
482579 return $ result ;
483580 }
484581
@@ -511,6 +608,9 @@ public function get_wc_categories( $current_values ) {
511608 $ used_categories = array ();
512609 if ( ! empty ( $ current_values ['productmeta_categories ' ] ) ) {
513610 $ used_categories = preg_split ( '/\r\n|\n/ ' , $ current_values ['productmeta_categories ' ] );
611+ if ( false === $ used_categories ) {
612+ $ used_categories = array ();
613+ }
514614 }
515615
516616 foreach ( $ product_categories as $ category ) {
@@ -615,8 +715,9 @@ public function get_db_field( $ppom_field_id ) {
615715 * @see ppom_admin_process_product_meta()
616716 */
617717 public function ppom_attach_ppoms () {
618- if ( ! isset ( $ _POST ['ppom_attached_nonce ' ] )
619- || ! wp_verify_nonce ( $ _POST ['ppom_attached_nonce ' ], 'ppom_attached_nonce_action ' )
718+ $ attached_nonce = isset ( $ _POST ['ppom_attached_nonce ' ] ) ? sanitize_text_field ( wp_unslash ( $ _POST ['ppom_attached_nonce ' ] ) ) : '' ;
719+ if ( '' === $ attached_nonce
720+ || ! wp_verify_nonce ( $ attached_nonce , 'ppom_attached_nonce_action ' )
620721 || ! ppom_security_role ()
621722 ) {
622723 $ response = array (
@@ -627,7 +728,7 @@ public function ppom_attach_ppoms() {
627728 wp_send_json ( $ response );
628729 }
629730
630- $ ppom_id = intval ( $ _POST ['ppom_id ' ] );
731+ $ ppom_id = isset ( $ _POST ['ppom_id ' ] ) ? absint ( $ _POST [ ' ppom_id ' ] ) : 0 ;
631732 $ is_pro_user = 'valid ' === apply_filters ( 'product_ppom_license_status ' , '' );
632733
633734 // +----- Attach Field to Product -----+
@@ -788,7 +889,7 @@ public function save_settings() {
788889 */
789890 public function ppom_setting_wpml ( $ value , $ option , $ raw_value ) {
790891
791- if ( isset ( $ option ['type ' ] ) && isset ( $ option [ ' type ' ] ) == ' text ' ) {
892+ if ( isset ( $ option ['type ' ] ) && ' text ' === $ option [ ' type ' ] ) {
792893 $ value = ppom_wpml_translate ( $ value , 'PPOM ' );
793894 }
794895
0 commit comments