* @return void */ private function __construct() { $this->api_instance = new \Affilizz\API(); add_action( 'wp_ajax_affilizz_get_publications', array( $this, 'get_publications' ) ); add_action( 'wp_ajax_affilizz_get_publication_contents', array( $this, 'get_publication_contents' ) ); // Handle ajax editing add_action( 'wp_ajax_edit_affilizz_publication_shortcode', array( $this, 'edit_shortcode_ajax' ) ); // Handle ajax creation add_action( 'wp_ajax_find_affilizz_offers_by_url', array( $this, 'find_offers_by_url' ) ); add_action( 'wp_ajax_find_affilizz_offers_by_keyword', array( $this, 'find_offers_by_keyword' ) ); add_action( 'wp_ajax_find_affilizz_offers_by_product', array( $this, 'find_offers_by_product' ) ); add_action( 'wp_ajax_affilizz_create_offer', array( $this, 'create_offer_ajax' ) ); // Handle ajax rendering add_action( 'wp_ajax_get_affilizz_publication', array( $this, 'get_publication_data_and_render' ) ); // Handle ajax publication ID resetting add_action( 'wp_ajax_affilizz_reset_publication_id', array( $this, 'reset_publication_id' ) ); } private function request_value( $key, $default = '' ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Centralized request accessor; callers sanitize based on expected shape. return isset( $_REQUEST[ $key ] ) ? wp_unslash( $_REQUEST[ $key ] ) : $default; } private function request_text( $key, $default = '' ) { return sanitize_text_field( $this->request_value( $key, $default ) ); } private function request_array( $key ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Centralized authenticated admin AJAX request accessor. return isset( $_REQUEST[ $key ] ) && is_array( $_REQUEST[ $key ] ) ? map_deep( wp_unslash( $_REQUEST[ $key ] ), 'sanitize_text_field' ) : array(); } /** * Retrieves the publications from the API. * @author Affilizz * @return void */ public function get_publications() { $current_id = $this->request_text( 'current_id' ); $force_refresh = $this->request_text( 'force', false ); $force_refresh = ! empty( $force_refresh ); $search_term = $this->request_text( 'search', false ); $currently_selected = \Affilizz\Util\Publications::get( $current_id ); if ( empty( $currently_selected ) ) { $currently_selected = new \stdClass(); } // Add the currently selected value $valid_publications = array(); if ( ! empty( $currently_selected->publication_id ) && ! empty( $currently_selected->publication_name ) ) { $valid_publications[ $currently_selected->publication_id ] = array( 'selected' => true, 'recent' => in_array( $currently_selected->publication_id, \Affilizz\Util\Publications::recent() ), 'label' => $currently_selected->publication_name, 'value' => $currently_selected->publication_id, ); } // Add the found publications $recent = \Affilizz\Util\Publications::recent(); foreach ( ( new \Affilizz\API() )->get_publications( $force_refresh, $search_term ) as $key => $publication ) { if ( empty( $publication['name'] ) ) { $publication['name'] = __( 'Untitled publication', 'affilizz' ); } $valid_publications[ $key ] = $publication; $valid_publications[ $key ] = array_merge( $valid_publications[ $key ], array( 'selected' => false, 'recent' => ( ! empty( $publication['recent'] ) && $publication['recent'] ), 'tag' => (string) ( ! empty( $publication['tag'] ) ? $publication['tag'] : '' ), 'id' => (string) ( ! empty( $publication['id'] ) ? $publication['id'] : '' ), 'class' => (string) ( ! empty( $publication['tag-class'] ) ? $publication['tag-class'] : '' ), 'label' => $publication['name'], 'value' => $key, ) ); } // Reduce the object to an array $valid_publications = array_values( $valid_publications ); echo wp_json_encode( array( // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Echoes sanitized request context for the authenticated admin UI. 'request' => map_deep( wp_unslash( $_REQUEST ), 'sanitize_text_field' ), 'currently_selected' => $currently_selected->publication_id ?? '', 'publications' => $valid_publications, 'force_refresh' => $force_refresh, ) ); wp_die(); } /** * Gets a structured representation of the contents in a publication (and their metadata from the database if applicable) * @author Affilizz * @param $context_id The ID of the tuple in the database from which to pull both the publication and its contents. * @return void */ public function get_publication_contents( $context_id = false ) { // Allow to pass the context through a regular call or an ajax variable if ( ! $context_id ) { $context_id = $this->request_text( 'context' ); } $publication = $this->request_text( 'publication_id' ); $context = $this->request_text( 'context_id' ); $channel = get_option( 'affilizz_channel' ); $publication_object = \Affilizz\Util\Publications::get( $context ); $selected_publication_contents = array(); // If we do not have a publication or if the publication is not the one in database (e.g. if we change publication in the modal), do not return selected content ids if ( is_object( $publication_object ) && ! empty( $publication_object->publication_id ) && $publication === $publication_object->publication_id ) { if ( ! empty( $publication_object->channel_id ) ) { $channel = $publication_object->channel_id; } if ( json_decode( stripslashes( $publication_object->publication_contents ) ) ) { $selected_publication_contents = json_decode( stripslashes( $publication_object->publication_contents ), true ); $selected_publication_contents = wp_list_pluck( $selected_publication_contents, 'id' ); } } $publication_api_object = $this->api_instance->get_publication( $publication, $channel ); $publication_contents = array(); // Added in 1.14.7 : Support for the deals as they are located in a different child object foreach ( $publication_api_object->containers as $container ) { $default_title = __( 'Deal', 'affilizz' ); $truncate = true; if ( ! empty( trim( $container->tinyCode ) ) ) { $default_title = __( 'Deal', 'affilizz' ) . ' (#' . $container->tinyCode . ')'; $truncate = false; } $name = trim( $container->name ) ?: $default_title; if ( $truncate ) { $name = substr( $name, 0, 30 ) . ( strlen( $name ) > 28 ? '…' : '' ); } $publication_contents[ $container->id ] = array( 'label' => esc_attr( $name ), 'value' => $container->id, 'name' => esc_attr( $name ), 'selected' => in_array( $container->id, $selected_publication_contents ), 'type' => strtolower( $container->type ), ); } // Build the array to return foreach ( $publication_api_object->contents as $content ) { $metadata = $content->items[0]->metadata; $name = trim( $metadata->name ) ?: __( 'Untitled publication content', 'affilizz' ); $publication_contents[ $content->id ] = array( 'label' => esc_attr( trim( substr( $name, 0, 50 ) ) . ( strlen( $name ) > 48 ? '…' : '' ) ), 'value' => $content->id, 'name' => esc_attr( trim( substr( $name, 0, 50 ) ) . ( strlen( $name ) > 48 ? '…' : '' ) ), 'selected' => in_array( $content->id, $selected_publication_contents ), 'type' => strtolower( $content->type ), 'type' => strtolower( $content->type ), ); } $publication_contents = array_values( $publication_contents ); // Return the JSON encoded version of the array echo wp_json_encode( $publication_contents ); wp_die(); } /** * Renders the shortcode attributes edition modal. * @author Affilizz * @return void */ public function edit_shortcode_ajax() { $id = $this->request_text( 'id' ); $object = \Affilizz\Util\Publications::get( $id ); if ( empty( $object ) ) { echo wp_json_encode( array( 'error' => __( 'No publication found', 'affilizz' ) ) ); die(); } // Code correction - see https://developer.wordpress.org/apis/security/data-validation/ $object = (object) $object; echo wp_json_encode( array( 'context_id' => $id, 'publication_id' => $object->publication_id, 'selected' => array( 'publication' => $object->publication_id, 'recent' => in_array( $object->publication_id, \Affilizz\Util\Publications::recent() ), 'label' => $object->publication_name, 'contents' => json_decode( stripslashes( $object->publication_contents ), true ), ), ) ); wp_die(); } /** * Renders the SSR content for a publication attributes edition modal. * @author Affilizz * @return void */ public function get_publication_data_and_render() { $id = $this->request_text( 'id' ); if ( ! empty( $id ) ) { $shortcode = new \Affilizz\Shortcode\Publication(); $result = $shortcode->get_publication_render( $id, true ); // Format correction - see https://developer.wordpress.org/apis/security/data-validation/ $with_indicator = (int) $this->request_value( 'with_indicator', 0 ); if ( $with_indicator ) { $result = '
' . PHP_EOL . '' . PHP_EOL . '' . esc_attr__( 'Affilizz logo', 'affilizz' ) . '' . PHP_EOL . '' . PHP_EOL . esc_html__( 'Affilizz affiliate content', 'affilizz' ) . PHP_EOL . '
' . PHP_EOL . $result; } wp_die( wp_kses( $result, \Affilizz\Core::get_extended_allowed_tags() ) ); } wp_die(); } /** * Finds publications by URL and returns them as a JSON object. * @return void * @since 1.15.0 * @author Affilizz */ public function find_offers_by_url() { $url = $this->request_text( 'url' ); $data = $this->api_instance->detect( $url ); $product_id = $data->productLocalizedIds ?? null; if ( is_array( $product_id ) ) { $product_id = $product_id[0] ?? null; } $found_offers = $product_id ? $this->api_instance->find_offers_by_product( $product_id ) : array(); $data->offers = $found_offers ?? array(); echo wp_json_encode( $data ); wp_die(); } /** * Finds publications by keyword and returns them as a JSON object. * @return void * @since 1.15.0 * @author Affilizz */ public function find_offers_by_keyword() { $keyword = $this->request_text( 'keyword' ); $min_price = intval( $this->request_value( 'min_price', 0 ) ); $max_price = intval( $this->request_value( 'max_price', 0 ) ); if ( $min_price > $max_price ) { $tmp = $min_price; $min_price = $max_price; $max_price = $tmp; } if ( ! empty( $keyword ) ) { $offers = $this->api_instance->find_offers_by_keyword( $keyword, $min_price, $max_price ); } $data = $offers->hits ?? array(); echo wp_json_encode( $data ); wp_die(); } /** * Finds offers by product ID and returns them as a JSON object. * @return void * @since 1.15.0 * @author Affilizz */ public function find_offers_by_product() { $product_id = $this->request_text( 'productLocalizedId' ); if ( ! empty( $product_id ) ) { $offers = $this->api_instance->find_offers_by_product( $product_id ); $offers = $this->format_found_offers_by_product( $offers ); } echo wp_json_encode( $offers ?? array() ); wp_die(); } /** * Formats the offers found by product localized ID. Code isolated to allow for different transformations in the future. * @param array $found_offers The offers found by product localized ID. * @return array The formatted offers. * @since 1.15.0 * @access private * @author Affilizz */ private function format_found_offers_by_product( $found_offers ) { $offers = array(); foreach ( $found_offers as $offer ) { $offers[] = array( 'id' => $offer->id, 'shop' => array( array( 'name' => $offer->shopName ?: __( 'Unknown shop', 'affilizz' ), 'icon' => $offer->shopIcon ?: '', 'price' => $offer->price ?: 0, 'currencyCode' => $offer->currencyCode ?: 'EUR', 'link' => $offer->link ?: '#', ), ), 'product' => array( 'localizedId' => $offer->productLocalizedId ?: '', 'name' => $offer->name ?: __( 'Unknown product', 'affilizz' ), 'image' => $offer->imageUrl ?: '', ), 'affiliated' => true, 'origin' => $offer->origin, 'condition' => $offer->condition, ); } return $offers; } /** * Create an offer from the Affilizz modal * @since 1.15.0 * @author Affilizz */ public function create_offer_ajax() { $errors = array(); $type = $this->request_text( 'type', 'link' ); $mandatory_fields = array( 'product' ); // Card-label is mandatory only on offer not detected context if ( 'card' === $type && empty( $this->request_text( 'product-localized-id' ) ) ) { $mandatory_fields[] = 'card-label'; } // Checking we have all mandatory fields foreach ( $mandatory_fields as $field ) { if ( empty( trim( $this->request_text( $field ) ) ) ) { $errors[ $field ][] = 'This field can not be empty.'; } } // Checking other fields $merchant_url = $this->request_text( 'merchant-url' ); if ( ! empty( $merchant_url ) && ! filter_var( $merchant_url, FILTER_VALIDATE_URL ) ) { $errors['merchant-url'][] = 'This URL seems to be incorrect.'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Authenticated admin AJAX endpoint; request is sanitized before API usage. $request = map_deep( wp_unslash( $_REQUEST ), 'sanitize_text_field' ); $request['card-positive-arguments'] = array_filter( $this->request_array( 'card-positive-arguments' ) ); $request['card-negative-arguments'] = array_filter( $this->request_array( 'card-negative-arguments' ) ); // Is there any errors? if ( ! empty( $errors ) ) { wp_send_json( array( 'type' => 'error_form', 'message' => 'There are errors in form', 'errors' => $errors, ), 400 ); } else { $current_publication_id = ! empty( $request['post_ID'] ) ? get_post_meta( intval( $request['post_ID'] ), 'affilizz_publication_id', true ) : null; if ( $current_publication_id ) { // Add content to existing offer $data = $this->api_instance->add_offer_contents( $current_publication_id, $type, $request ); } else { // Creating offer! $data = $this->api_instance->create_offer( $type, $request ); // Saving publication id if current one is empty if ( ! empty( $data->id ) && ! empty( $request['post_ID'] ) ) { add_post_meta( intval( $request['post_ID'] ), 'affilizz_publication_id', $data->id ); } } wp_send_json( $data ); } wp_die(); } /** * Remove the saved publication ID * @since 1.15.0 * @author Affilizz */ public function reset_publication_id() { $post_id = intval( $this->request_value( 'post_id', 0 ) ); $deleted = false; if ( ! empty( $post_id ) ) { $deleted = delete_post_meta( $post_id, 'affilizz_publication_id' ); } wp_send_json( array( 'deleted' => $deleted ) ); } }