Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/another-wordpress-classifieds-plugin/awpcp.php
+++ b/another-wordpress-classifieds-plugin/awpcp.php
@@ -5,7 +5,7 @@
* Plugin Name: AWP Classifieds
* Plugin URI: https://awpcp.com/
* Description: Run a free or paid classified ads service on your WordPress site.
- * Version: 4.4.7
+ * Version: 4.4.8
* Author: AWP Classifieds Team
* Author URI: https://awpcp.com/
* License: GPLv2 or later
@@ -61,7 +61,7 @@
global $hasregionsmodule;
global $hasextrafieldsmodule;
-$awpcp_db_version = '4.4.7';
+$awpcp_db_version = '4.4.8';
$awpcp_imagesurl = AWPCP_URL . '/resources/images';
$hascaticonsmodule = 0;
--- a/another-wordpress-classifieds-plugin/frontend/class-categories-switcher.php
+++ b/another-wordpress-classifieds-plugin/frontend/class-categories-switcher.php
@@ -38,7 +38,7 @@
* @param array $params An array of parameters for the Categories Switcher component.
*/
public function render( $params = array() ) {
- if ( $this->query->is_browse_listings_page() || $this->query->is_browse_categories_page() ) {
+ if ( $this->query->is_browse_listings_page() ) {
$action_url = awpcp_current_url();
} else {
$action_url = awpcp_get_browse_categories_page_url();
--- a/another-wordpress-classifieds-plugin/frontend/class-query.php
+++ b/another-wordpress-classifieds-plugin/frontend/class-query.php
@@ -50,6 +50,13 @@
return $this->is_page_that_has_shortcode( 'AWPCPREPLYTOAD' );
}
+ /**
+ * @since 4.4.8
+ */
+ public function is_user_listings_page() {
+ return $this->is_page_that_has_shortcode( 'AWPCPUSERLISTINGS' );
+ }
+
public function is_browse_listings_page() {
return $this->is_page_that_has_shortcode( 'AWPCPBROWSEADS' );
}
@@ -71,7 +78,7 @@
}
public function is_browse_categories_page() {
- return $this->is_browse_listings_page();
+ return $this->is_page_that_has_shortcode( 'AWPCPBROWSECATS' );
}
public function is_renew_listing_page() {
--- a/another-wordpress-classifieds-plugin/frontend/class-url-backwards-compatibility-redirection-helper.php
+++ b/another-wordpress-classifieds-plugin/frontend/class-url-backwards-compatibility-redirection-helper.php
@@ -122,7 +122,7 @@
return;
}
- if ( $this->query->is_browse_listings_page() || $this->query->is_browse_categories_page() ) {
+ if ( $this->query->is_browse_listings_page() ) {
$this->maybe_redirect_browse_listings_request();
return;
}
--- a/another-wordpress-classifieds-plugin/frontend/page-place-ad.php
+++ b/another-wordpress-classifieds-plugin/frontend/page-place-ad.php
@@ -889,7 +889,7 @@
$ui['allow-regions-modification'] = $is_moderator || !$edit || get_awpcp_option( 'allow-regions-modification' );
$ui['price-field'] = get_awpcp_option('displaypricefield') == 1;
$ui['extra-fields'] = $hasextrafieldsmodule && function_exists( 'awpcp_extra_fields_module' );
- $ui['terms-of-service'] = !$edit && !$is_moderator && get_awpcp_option('requiredtos');
+ $ui['terms-of-service'] = ! $edit && get_awpcp_option( 'requiredtos' );
$ui['captcha'] = !$edit && !is_admin() && ( get_awpcp_option( 'captcha-enabled-in-place-listing-form' ) == 1 );
$hidden['step'] = 'save-details';
@@ -1159,8 +1159,8 @@
}
// Terms of service required and accepted?
- if (!$edit && !$is_moderator && get_awpcp_option('requiredtos') && empty($data['terms-of-service'])) {
- $errors['terms-of-service'] = __("You did not accept the terms of service", 'another-wordpress-classifieds-plugin');
+ if ( ! $edit && get_awpcp_option( 'requiredtos' ) && empty( $data['terms-of-service'] ) ) {
+ $errors['terms-of-service'] = __( 'You did not accept the terms of service', 'another-wordpress-classifieds-plugin' );
}
if ( !$edit && !is_admin() && get_awpcp_option( 'captcha-enabled-in-place-listing-form' ) ) {
--- a/another-wordpress-classifieds-plugin/frontend/shortcode.php
+++ b/another-wordpress-classifieds-plugin/frontend/shortcode.php
@@ -64,7 +64,7 @@
add_shortcode( 'AWPCPPAYMENTTHANKYOU', array( $this, 'noop' ) );
add_shortcode( 'AWPCPCANCELPAYMENT', array( $this, 'noop' ) );
- add_shortcode( 'AWPCPBROWSECATS', array( $this->browse_ads, 'dispatch' ) );
+ add_shortcode( 'AWPCPBROWSECATS', array( $this, 'browse_categories' ) );
add_shortcode( 'AWPCPBROWSEADS', array( $this->browse_ads, 'dispatch' ) );
add_shortcode( 'AWPCPSHOWAD', array( $this, 'show_ad' ) );
@@ -141,6 +141,25 @@
return $this->output['search-ads'];
}
+ /**
+ * Renders the View Categories list (same UI as main-page layout=2).
+ *
+ * Previously aliased to Browse Ads for backwards compatibility.
+ *
+ * @since 4.4.8
+ *
+ * @return string
+ */
+ public function browse_categories() {
+ if ( ! isset( $this->output['browse-categories'] ) ) {
+ awpcp_enqueue_main_script();
+
+ $this->output['browse-categories'] = awpcp_display_the_classifieds_page_body( '' );
+ }
+
+ return $this->output['browse-categories'];
+ }
+
public function reply_to_ad() {
if ( ! isset( $this->output['reply-to-ad'] ) ) {
do_action( 'awpcp-shortcode', 'reply-to-ad' );
@@ -492,7 +511,7 @@
}
if ( $show_browse_ads_item ) {
- if ( awpcp_is_browse_listings_page() || awpcp_is_browse_categories_page() ) {
+ if ( awpcp_is_browse_listings_page() ) {
if ( get_awpcp_option( 'main_page_display' ) ) {
$browse_cats_url = awpcp_get_view_categories_url();
} else {
--- a/another-wordpress-classifieds-plugin/frontend/templates/email-ad-enabled-user.tpl.php
+++ b/another-wordpress-classifieds-plugin/frontend/templates/email-ad-enabled-user.tpl.php
@@ -4,20 +4,21 @@
}
-// emails are sent in plain text, trailing whitespace are required for proper formatting
-// translators: %s is the contact name
-printf( esc_html__( 'Hello %s,', 'another-wordpress-classifieds-plugin'), esc_html( $contact_name ) );
-?>
+// Emails are sent in plain text, blank lines are required for proper formatting.
+printf(
+ // translators: %s is the contact name.
+ awpcp_esc_plaintext( __( 'Hello %s,', 'another-wordpress-classifieds-plugin' ) ),
+ awpcp_esc_plaintext( $contact_name )
+);
+echo PHP_EOL . PHP_EOL;
-<?php
printf(
- // translators: %1$s is the listing title, %2$s is the listing URL
- esc_html__( 'Your Ad "%1$s" was recently approved by the admin. You should be able to see the Ad published here: %2$s.', 'another-wordpress-classifieds-plugin' ),
- esc_html( $listing_title ),
+ // translators: %1$s is the listing title, %2$s is the listing URL.
+ awpcp_esc_plaintext( __( 'Your Ad "%1$s" was recently approved by the admin. You should be able to see the Ad published here: %2$s.', 'another-wordpress-classifieds-plugin' ) ),
+ awpcp_esc_plaintext( $listing_title ),
esc_url_raw( get_permalink( $listing->ID ) )
);
-?>
+echo PHP_EOL . PHP_EOL;
-<?php echo esc_html( awpcp_get_blog_name() ); ?>
-<?php
+echo awpcp_esc_plaintext( awpcp_get_blog_name() ) . PHP_EOL;
echo esc_url_raw( home_url() );
--- a/another-wordpress-classifieds-plugin/frontend/templates/email-send-all-ad-access-keys.tpl.php
+++ b/another-wordpress-classifieds-plugin/frontend/templates/email-send-all-ad-access-keys.tpl.php
@@ -15,8 +15,13 @@
<?php foreach ( $ads as $ad ): ?>
<?php echo esc_html( $listing_renderer->get_listing_title( $ad ) ); ?>
-<?php esc_html_e( 'Access Key', 'another-wordpress-classifieds-plugin' ); ?>: <?php echo esc_html( $listing_renderer->get_access_key( $ad ) ); ?>
-<?php esc_html_e( 'Edit Link:', 'another-wordpress-classifieds-plugin' ); ?> <?php echo esc_url_raw( awpcp_get_edit_listing_url_with_access_key( $ad ) ); ?>
+
+<?php esc_html_e( 'Access Key', 'another-wordpress-classifieds-plugin' ); ?>:
+<?php echo esc_html( $listing_renderer->get_access_key( $ad ) ); ?>
+
+<?php esc_html_e( 'Edit Link:', 'another-wordpress-classifieds-plugin' ); ?>
+<?php echo esc_url_raw( awpcp_get_edit_listing_url_with_access_key( $ad ) ); ?>
+
<?php endforeach; ?>
--- a/another-wordpress-classifieds-plugin/includes/class-authentication-redirection-handler.php
+++ b/another-wordpress-classifieds-plugin/includes/class-authentication-redirection-handler.php
@@ -32,6 +32,8 @@
$page_requires_authentication = $this->post_listing_page_requires_authentication();
} elseif ( $this->query->is_reply_to_listing_page() ) {
$page_requires_authentication = $this->reply_to_listing_page_requires_autentication();
+ } elseif ( $this->query->is_user_listings_page() ) {
+ $page_requires_authentication = $this->user_listings_page_requires_authentication();
} else {
$page_requires_authentication = false;
}
@@ -49,6 +51,13 @@
return $this->settings->get_option( 'reply-to-ad-requires-registration' );
}
+ /**
+ * @since 4.4.8
+ */
+ private function user_listings_page_requires_authentication() {
+ return $this->settings->get_option( 'requireuserregistration' );
+ }
+
private function redirect_to_login_page( $login_url ) {
wp_safe_redirect( add_query_arg( 'redirect_to', urlencode( awpcp_current_url() ), $login_url ) );
exit();
--- a/another-wordpress-classifieds-plugin/includes/class-container-configuration.php
+++ b/another-wordpress-classifieds-plugin/includes/class-container-configuration.php
@@ -106,7 +106,6 @@
$container['FormFieldsValidator'] = $container->service( function( $container ) {
return new AWPCP_FormFieldsValidator(
$container['ListingAuthorization'],
- $container['RolesAndCapabilities'],
$container['Settings']
);
} );
--- a/another-wordpress-classifieds-plugin/includes/class-listings-api.php
+++ b/another-wordpress-classifieds-plugin/includes/class-listings-api.php
@@ -737,7 +737,15 @@
}
if ( get_awpcp_option( 'imagesapprove' ) == 1 ) {
- $alerts[] = __( 'If you have uploaded images your images will not show up until an admin has approved them.', 'another-wordpress-classifieds-plugin' );
+ $payment_term = $this->listing_renderer->get_payment_term( $ad );
+
+ if (
+ awpcp_are_images_allowed()
+ && is_object( $payment_term )
+ && (int) $payment_term->images > 0
+ ) {
+ $alerts[] = __( 'If you have uploaded images your images will not show up until an admin has approved them.', 'another-wordpress-classifieds-plugin' );
+ }
}
return $alerts;
--- a/another-wordpress-classifieds-plugin/includes/constructor-functions.php
+++ b/another-wordpress-classifieds-plugin/includes/constructor-functions.php
@@ -194,7 +194,6 @@
return new AWPCP_TermsOfServiceFormField(
$slug,
- $container['RolesAndCapabilities'],
$container['Settings'],
$container['TemplateRenderer']
);
--- a/another-wordpress-classifieds-plugin/includes/form-fields/class-form-fields-validator.php
+++ b/another-wordpress-classifieds-plugin/includes/form-fields/class-form-fields-validator.php
@@ -18,11 +18,6 @@
private $authorization;
/**
- * @var AWPCP_RolesAndCapabilities
- */
- private $roles;
-
- /**
* @var object
*/
private $settings;
@@ -30,13 +25,11 @@
/**
* @since 4.0.0
*
- * @param object $authorization An instance of Listing Authorization.
- * @param object $roles An instance of Roles and Capabilities.
- * @param object $settings An instance of Settings API.
+ * @param object $authorization An instance of Listing Authorization.
+ * @param object $settings An instance of Settings API.
*/
- public function __construct( $authorization, $roles, $settings ) {
+ public function __construct( $authorization, $settings ) {
$this->authorization = $authorization;
- $this->roles = $roles;
$this->settings = $settings;
}
@@ -136,7 +129,7 @@
}
}
- if ( $this->settings->get_option( 'requiredtos' ) && ! $this->roles->current_user_is_moderator() ) {
+ if ( $this->settings->get_option( 'requiredtos' ) ) {
if ( $data['terms_of_service'] !== 'accepted' ) {
$errors['terms_of_service'] = __( 'Please read and accept the Terms of Service.', 'another-wordpress-classifieds-plugin' );
}
--- a/another-wordpress-classifieds-plugin/includes/form-fields/class-terms-of-service-form-field.php
+++ b/another-wordpress-classifieds-plugin/includes/form-fields/class-terms-of-service-form-field.php
@@ -15,11 +15,6 @@
private $template = 'frontend/form-fields/terms-of-service-form-field.tpl.php';
/**
- * @var AWPCP_RolesAndCapabilities
- */
- private $roles;
-
- /**
* @var AWPCP_Settings_API
*/
private $settings;
@@ -32,10 +27,9 @@
/**
* @since 4.0.2
*/
- public function __construct( $slug, $roles, $settings, $template_renderer ) {
+ public function __construct( $slug, $settings, $template_renderer ) {
parent::__construct( $slug );
- $this->roles = $roles;
$this->settings = $settings;
$this->template_renderer = $template_renderer;
}
@@ -67,10 +61,6 @@
return false;
}
- if ( $this->roles->current_user_is_moderator() ) {
- return false;
- }
-
return true;
}
--- a/another-wordpress-classifieds-plugin/includes/listings/class-delete-listing-event-listener.php
+++ b/another-wordpress-classifieds-plugin/includes/listings/class-delete-listing-event-listener.php
@@ -41,7 +41,7 @@
add_action( 'untrashed_post', [ $this, 'after_untrash_post' ] );
add_action( 'before_delete_post', [ $this, 'before_delete_post' ] );
- add_action( 'after_delete_post', [ $this, 'after_delete_post' ] );
+ add_action( 'after_delete_post', [ $this, 'after_delete_post' ], 10, 2 );
}
/**
@@ -98,9 +98,27 @@
}
/**
- * @since 4.0.0
- */
- public function after_delete_post( $post_id ) {
+ * Fires after a post is permanently deleted.
+ *
+ * Uses the WP_Post object passed by WordPress because get_post() returns
+ * null after the post row has been removed.
+ *
+ * @since 4.0.0
+ * @since 4.4.8 Accepts the deleted WP_Post from after_delete_post.
+ *
+ * @param int $post_id Post ID.
+ * @param WP_Post|null $post Deleted post object (available since WP 5.5).
+ */
+ public function after_delete_post( $post_id, $post = null ) {
+ if ( $post instanceof WP_Post ) {
+ if ( $this->listing_post_type !== $post->post_type ) {
+ return;
+ }
+
+ do_action( 'awpcp_delete_ad', $post );
+ return;
+ }
+
$this->maybe_do_action( 'awpcp_delete_ad', $post_id );
}
}
--- a/another-wordpress-classifieds-plugin/includes/models/payment-transaction.php
+++ b/another-wordpress-classifieds-plugin/includes/models/payment-transaction.php
@@ -305,7 +305,12 @@
}
/**
+ * Verify that the payment can be marked as completed.
+ *
+ * @since 4.4.8
+ *
* @param array &$errors
+ * @return bool
*/
public function verify_payment_completed_conditions(&$errors) {
if (empty($this->payment_status)) {
@@ -313,6 +318,11 @@
return false;
}
+ if ( $this->payment_is_not_verified() ) {
+ $errors[] = __( 'The payment for this transaction has not been verified yet.', 'another-wordpress-classifieds-plugin' );
+ return false;
+ }
+
return true;
}
--- a/another-wordpress-classifieds-plugin/includes/payment-gateway-paypal-standard.php
+++ b/another-wordpress-classifieds-plugin/includes/payment-gateway-paypal-standard.php
@@ -69,10 +69,7 @@
$transaction->errors['verification-get'] = $errors;
} elseif ( 'INVALID' === $response ) {
- // INVALID on user return is likely a timing issue. Show pending message.
- $transaction->set( 'pending_verification', true );
-
- // Don't set errors - we'll show a pending notice instead.
+ // The caller determines whether this is a return or an IPN.
unset( $transaction->errors['verification-get'] );
unset( $transaction->errors['verification-post'] );
} elseif ( 'ERROR' === $response ) {
@@ -119,6 +116,15 @@
$custom = awpcp_get_var( array( 'param' => 'custom' ), 'post' );
$payer_email = awpcp_get_var( array( 'param' => 'payer_email' ), 'post' );
+ if ( strcasecmp( (string) $custom, (string) $transaction->id ) !== 0 ) {
+ $message = __( 'The payment transaction could not be verified. Please contact customer service for assistance.', 'another-wordpress-classifieds-plugin' );
+ $transaction->errors['validation'] = $message;
+ $transaction->payment_status = AWPCP_Payment_Transaction::PAYMENT_STATUS_INVALID;
+ $transaction->set( 'verified', false );
+ awpcp_payment_failed_email( $transaction, $message );
+ return false;
+ }
+
// this variables are not used for verification purposes.
$item_name = awpcp_get_var( array( 'param' => 'item_name' ), 'post' );
$item_number = awpcp_get_var( array( 'param' => 'item_number' ), 'post' );
@@ -296,6 +302,7 @@
*/
private function do_process_payment( $transaction, $is_ipn ) {
if ( $transaction->get( 'verified', false ) ) {
+ $transaction->set( 'pending_verification', false );
return;
}
@@ -316,9 +323,10 @@
if ( $is_ipn ) {
// IPN returning INVALID is a real failure from PayPal.
$transaction->payment_status = AWPCP_Payment_Transaction::PAYMENT_STATUS_INVALID;
+ $transaction->set( 'pending_verification', false );
} else {
- // User return with INVALID is likely a timing issue. Set to PENDING and wait for IPN.
- $transaction->payment_status = AWPCP_Payment_Transaction::PAYMENT_STATUS_PENDING;
+ // Wait for an IPN instead of treating an unverified return as payment pending.
+ $transaction->payment_status = AWPCP_Payment_Transaction::PAYMENT_STATUS_NOT_VERIFIED;
$transaction->set( 'pending_verification', true );
}
} elseif ( 'ERROR' === $response ) {
--- a/another-wordpress-classifieds-plugin/includes/payments-api.php
+++ b/another-wordpress-classifieds-plugin/includes/payments-api.php
@@ -540,7 +540,8 @@
}
public function process_payment_completed($transaction, $redirect=true) {
- $errors = array();
+ $errors = array();
+ $pending_verification = $transaction->get( 'pending_verification', false );
/**
* Only attempt to complete the payment if we are in a previous state.
@@ -548,7 +549,7 @@
* IPN notifications are likely to be associated to transactions that
* are already completed.
*/
- if (!$transaction->is_payment_completed() && !$transaction->is_completed()) {
+ if ( ! $pending_verification && ! $transaction->is_payment_completed() && ! $transaction->is_completed() ) {
$this->set_transaction_status_to_payment_completed($transaction, $errors);
if (!empty($errors)) {
@@ -558,11 +559,13 @@
}
}
- try {
- $this->process_transaction( $transaction );
- } catch ( AWPCP_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
- // We simply ignore exceptions here because we are currently using them
- // in the Coupons module only for transactions that are doing checkout.
+ if ( ! $pending_verification ) {
+ try {
+ $this->process_transaction( $transaction );
+ } catch ( AWPCP_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+ // We simply ignore exceptions here because we are currently using them
+ // in the Coupons module only for transactions that are doing checkout.
+ }
}
$transaction->save();
@@ -931,7 +934,7 @@
$text = '';
$pending_verification = $transaction->get( 'pending_verification', false );
- if ( $pending_verification && $transaction->payment_is_pending() ) {
+ if ( $pending_verification && $transaction->payment_is_not_verified() ) {
// Payment verification returned INVALID on user return - likely a timing issue.
// Show a friendly pending message and auto-refresh to wait for IPN.
$title = __( 'Verifying Your Payment', 'another-wordpress-classifieds-plugin' );
@@ -1009,7 +1012,7 @@
public function render_payment_completed_page_title($transaction) {
$pending_verification = $transaction->get( 'pending_verification', false );
- if ( $pending_verification && $transaction->payment_is_pending() ) {
+ if ( $pending_verification && $transaction->payment_is_not_verified() ) {
return __( 'Verifying Your Payment', 'another-wordpress-classifieds-plugin' );
} elseif ($transaction->was_payment_successful()) {
return __( 'Payment Completed', 'another-wordpress-classifieds-plugin');
--- a/another-wordpress-classifieds-plugin/includes/regions-api.php
+++ b/another-wordpress-classifieds-plugin/includes/regions-api.php
@@ -47,15 +47,21 @@
return $this->db->get_col( $this->db->prepare( $sql, $parent_name ) );
}
- public function save($region) {
- if ( ! isset( $region['ad_id'] ) || empty( $region['ad_id'] ) ) {
+ public function save( $region ) {
+ $region = $this->filter_region_columns( stripslashes_deep( $region ) );
+
+ $region['ad_id'] = absint( isset( $region['ad_id'] ) ? $region['ad_id'] : 0 );
+
+ if ( empty( $region['ad_id'] ) ) {
return false;
}
- $region = stripslashes_deep( $region );
+ $region_id = intval( awpcp_array_data( 'id', 0, $region ) );
+
+ unset( $region['id'] );
- if ( intval( awpcp_array_data( 'id', null, $region ) ) > 0 ) {
- $result = $this->db->update( AWPCP_TABLE_AD_REGIONS, $region, array( 'id' => $region['id'] ) );
+ if ( $region_id > 0 ) {
+ $result = $this->db->update( AWPCP_TABLE_AD_REGIONS, $region, array( 'id' => $region_id ) );
} else {
$result = $this->db->insert( AWPCP_TABLE_AD_REGIONS, $region );
}
@@ -63,6 +69,47 @@
return $result !== false;
}
+ /**
+ * Allowlist region array keys to valid DB columns only.
+ *
+ * @since 4.4.8
+ *
+ * @param mixed $region Region data.
+ *
+ * @return array
+ */
+ private function filter_region_columns( $region ) {
+ if ( ! is_array( $region ) ) {
+ return array();
+ }
+
+ $allowed = array( 'id', 'ad_id', 'country', 'county', 'state', 'city', 'region_id' );
+
+ return array_intersect_key( $region, array_flip( $allowed ) );
+ }
+
+ /**
+ * Sanitise a user-submitted region to editable fields only.
+ *
+ * @since 4.4.8
+ *
+ * @param mixed $region Region data.
+ *
+ * @return array
+ */
+ private function prepare_submitted_region( $region ) {
+ if ( ! is_array( $region ) ) {
+ return array();
+ }
+
+ $allowed = array( 'country', 'county', 'state', 'city', 'region_id' );
+ $region = array_intersect_key( $region, array_flip( $allowed ) );
+
+ $region = array_filter( $region, 'is_scalar' );
+
+ return array_map( 'trim', $region );
+ }
+
public function delete_by_ad_id($ad_id) {
$result = $this->db->query( $this->db->prepare( "DELETE FROM " . AWPCP_TABLE_AD_REGIONS . " WHERE ad_id = %s", $ad_id ) );
return $result !== false;
@@ -79,18 +126,21 @@
}
public function update_ad_regions( $ad, $regions, $max_regions = 1 ) {
- // remove existing regions before adding the new ones
$this->delete_by_ad_id( $ad->ID );
$count = 0;
- foreach ($regions as $region) {
- if ( empty( implode( $region ) ) ) {
+
+ foreach ( $regions as $region ) {
+ $data = $this->prepare_submitted_region( $region );
+
+ if ( empty( implode( $data ) ) ) {
continue;
}
- if ($count < $max_regions) {
- $data = array_map( 'trim', $region );
- $this->save( array_merge( array( 'ad_id' => $ad->ID ), $data ) );
+
+ if ( $count < $max_regions ) {
+ $this->save( array_merge( $data, array( 'ad_id' => $ad->ID ) ) );
}
+
++$count;
}
}