: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* @author Advanced Ads <info@wpadvancedads.com>
namespace AdvancedAds\Admin\Pages;
use Advanced_Ads\Ad_Repository;
use AdvancedAds\Admin\Groups_List_Table;
use AdvancedAds\Entities;
use AdvancedAds\Framework\Utilities\Params;
use AdvancedAds\Interfaces\Screen_Interface;
use AdvancedAds\Utilities\WordPress;
defined( 'ABSPATH' ) || exit;
class Groups implements Screen_Interface {
private $list_table = null;
* Register screen into WordPress admin area.
public function register_screen(): void {
__( 'Ad Groups & Rotations', 'advanced-ads' ),
__( 'Groups & Rotation', 'advanced-ads' ),
WordPress::user_cap( 'advanced_ads_edit_ads' ),
add_action( 'in_admin_header', [ $this, 'get_list_table' ] );
if ( 'advanced-ads-groups' === Params::get( 'page' ) ) {
add_action( 'admin_init', [ $this, 'admin_init' ] );
* Intercept group form submission on dashboard init.
public function admin_init() {
'advanced-ads-groups' !== Params::get( 'page' )
&& ! Params::post( 'advads-group-update-nonce' )
&& ! Params::post( 'advads-group-add-nonce' )
&& 'delete' === Params::get( 'action' )
// Just skip if no group created, deleted, or edited.
$result = $this->handle_action();
$url = admin_url( 'admin.php?page=advanced-ads-groups' );
switch ( $result['code'] ) {
wp_redirect( "$url&message={$result['code']}&group={$result['group']}" );
wp_redirect( "$url&message={$result['code']}" );
wp_redirect( "$url&message=" . rawurlencode( $result['message'] ) );
public function handle_messages() {
$message = Params::get( 'message' );
if ( false === $message ) {
$message = sanitize_text_field( wp_unslash( $message ) );
echo '<div class="wrap">';
echo '<div class="notice inline"><p>' . esc_html__( 'Ad Group successfully created', 'advanced-ads' ) . '</p></div>';
window.addEventListener( 'DOMContentLoaded', () => {
window.location.hash = '#modal-group-edit-<?php echo esc_html( sanitize_text_field( wp_unslash( Params::get( 'group' ) ) ) ); ?>';
echo '<div id="message" class="updated inline"><p>' . esc_html__( 'Ad Groups successfully updated', 'advanced-ads' ) . '</p></div>';
echo '<div id="message" class="notice error inline"><p>' . esc_html( rawurldecode( $message ) ) . '</p></div>';
* Display screen content.
public function display(): void {
$this->handle_messages();
$taxonomy = get_taxonomy( Entities::TAXONOMY_AD_GROUP );
$wp_list_table = $this->get_list_table();
$is_search = Params::get( 's' );
include ADVADS_ABSPATH . 'views/admin/screens/groups.php';
* @return null|Groups_List_Table
public function get_list_table() {
$screen = get_current_screen();
if ( 'advanced-ads_page_advanced-ads-groups' === $screen->id && null === $this->list_table ) {
$screen->taxonomy = Entities::TAXONOMY_AD_GROUP;
$screen->post_type = Entities::POST_TYPE_AD;
$this->list_table = new Groups_List_Table();
return $this->list_table;
private function handle_action(): array {
$taxonomy = get_taxonomy( Entities::TAXONOMY_AD_GROUP );
$action = WordPress::current_action();
if ( Params::request( 'advads-group-add-nonce' ) ) {
if ( Params::request( 'advads-group-update-nonce' ) ) {
$handle = "handle_{$action}";
$group_id = Params::request( 'group_id', 0, FILTER_VALIDATE_INT );
if ( method_exists( $this, $handle ) ) {
$result = $this->$handle( $group_id, $taxonomy );
if ( is_wp_error( $result ) ) {
'message' => $result->get_error_message(),
} elseif ( 'create' === $action ) {
} elseif ( 'update' === $action ) {
* Handle add new or update group.
* @param int $group_id Group id.
* @param WP_Taxonomy $taxonomy Taxonomy instance.
private function handle_editedgroup( $group_id, $taxonomy ) {
check_admin_referer( 'update-group_' . $group_id );
if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to access this feature.', 'advanced-ads' ) );
return wp_insert_term( Params::post( 'name' ), Entities::TAXONOMY_AD_GROUP, $_POST );
$tag = get_term( $group_id, Entities::TAXONOMY_AD_GROUP );
wp_die( esc_html__( 'You attempted to edit an ad group that doesn’t exist. Perhaps it was deleted?', 'advanced-ads' ) );
return wp_update_term( $group_id, Entities::TAXONOMY_AD_GROUP, $_POST );
* @param int $group_id Group id.
* @param WP_Taxonomy $taxonomy Taxonomy instance.
private function handle_delete( $group_id, $taxonomy ): void {
check_admin_referer( 'delete-tag_' . $group_id );
if ( ! current_user_can( $taxonomy->cap->delete_terms ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to access this feature.', 'advanced-ads' ) );
wp_delete_term( $group_id, Entities::TAXONOMY_AD_GROUP );
Advanced_Ads_Group::delete_ad_weights( $group_id );
public function handle_update() {
if ( ! wp_verify_nonce( Params::post( 'advads-group-update-nonce' ), 'update-advads-groups' ) ) {
return new WP_Error( 'invalid_ad_group', __( 'Invalid Ad Group', 'advanced-ads' ) );
if ( ! WordPress::user_can( 'advanced_ads_edit_ads' ) ) {
return new WP_Error( 'invalid_ad_group_rights', __( 'You don’t have permission to change the ad groups', 'advanced-ads' ) );
* edit: emptying disabled, because when only a few groups are saved (e.g. when filtered by search), options are reset
* todo: needs a solution that also removes options when the group is removed
$all_weights = get_option( 'advads-ad-weights', [] );
$ad_groups_assoc = $this->update_remove_groups( $all_weights );
$this->loop_ad_groups( $all_weights, $ad_groups_assoc );
update_option( 'advads-ad-weights', $all_weights );
* @return Advanced_Ads_Group|WP_Error
public function handle_create() {
if ( ! wp_verify_nonce( Params::post( 'advads-group-add-nonce' ), 'add-advads-groups' ) ) {
return new WP_Error( 'invalid_ad_group', __( 'Invalid Ad Group', 'advanced-ads' ) );
if ( ! WordPress::user_can( 'advanced_ads_edit_ads' ) ) {
return new WP_Error( 'invalid_ad_group_rights', __( 'You don’t have permission to change the ad groups', 'advanced-ads' ) );
$group_name = Params::post( 'advads-group-name' );
if ( empty( $group_name ) ) {
return new WP_Error( 'no_ad_group_created', __( 'No ad group created', 'advanced-ads' ) );
$title = sanitize_text_field( wp_unslash( $group_name ) );
$new_group = wp_create_term( $title, Entities::TAXONOMY_AD_GROUP );
if ( is_wp_error( $new_group ) ) {
$posted_type = Params::post( 'advads-group-type' );
if ( ! empty( $posted_type ) && wp_advads()->group_manager->has_type( $posted_type ) ) {
$group = new Advanced_Ads_Group( $new_group['term_id'] );
$attributes = apply_filters(
'advanced-ads-group-save-atts',
$group->save( $attributes );
* Remove group in update routine
* @param array $all_weights Array of weights.
private function update_remove_groups( &$all_weights ): array {
$remove_ads = Params::post( 'advads-groups-removed-ads', false, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
$remove_ads_gid = Params::post( 'advads-groups-removed-ads-gid', false, FILTER_DEFAULT, FILTER_REQUIRE_ARRAY );
if ( $remove_ads && $remove_ads_gid && is_array( $remove_ads ) ) {
$len = count( $remove_ads );
for ( $i = 0; $i < $len; $i++ ) {
$ad_id = absint( wp_unslash( $remove_ads[ $i ] ) );
$group_id = absint( wp_unslash( $remove_ads_gid[ $i ] ) );
$ad_groups[ $ad_id ] = [];
if ( isset( $all_weights[ $group_id ] ) && isset( $all_weights[ $group_id ][ $ad_id ] ) ) {
unset( $all_weights[ $group_id ][ $ad_id ] );
// We need to load all the group ids, that are allocated to this ad and then remove the right one only.
$group_ids = $this->get_groups_by_ad_id( $ad_id );
foreach ( $group_ids as $gid ) {
if ( $gid !== $group_id ) {
$ad_groups[ $ad_id ][] = $gid;
* @param array $all_weights Array of weights.
* @param array $ad_groups_assoc Array of groups.
private function loop_ad_groups( &$all_weights, $ad_groups_assoc ): void {
$post_ad_groups = wp_unslash( Params::post( 'advads-groups', [], FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ) );
if ( empty( $post_ad_groups ) ) {
foreach ( $post_ad_groups as $_group_id => $_group ) {
wp_update_term( $_group_id, Entities::TAXONOMY_AD_GROUP, $_group );
$group = new Advanced_Ads_Group( $_group['id'] );
if ( isset( $_group['ads'] ) && is_array( $_group['ads'] ) ) {
foreach ( $_group['ads'] as $_ad_id => $_ad_weight ) {
* Check if this ad is representing the current group and remove it in this case
* could cause an infinite loop otherwise
* see also /classes/ad_type_group.php::remove_from_ad_group()
$ad = Ad_Repository::get( $_ad_id );
if ( ! isset( $ad_groups_assoc[ $_ad_id ] ) ) {
$ad_groups_assoc[ $_ad_id ] = $this->get_groups_by_ad_id( $_ad_id );
&& isset( $ad->output['group_id'] )
&& absint( $ad->output['group_id'] ) === $_group_id
unset( $_group['ads'][ $_ad_id ] );
$ad_groups_assoc[ $_ad_id ][] = (int) $_group_id;
$all_weights[ $group->id ] = $this->sanitize_ad_weights( $_group['ads'] );
// Save other attributes.
$type = isset( $_group['type'] ) ? $_group['type'] : 'default';
$ad_count = isset( $_group['ad_count'] ) ? $_group['ad_count'] : 1;
$options = isset( $_group['options'] ) ? $_group['options'] : [];
// allow other add-ons to save their own group attributes.
'advanced-ads-group-save-atts',
foreach ( $ad_groups_assoc as $_ad_id => $group_ids ) {
wp_set_object_terms( $_ad_id, $group_ids, Entities::TAXONOMY_AD_GROUP );
* Load groups with a given ad in them.
* @param integer $ad_id ad ID.
private function get_groups_by_ad_id( $ad_id ) {
$terms = wp_get_object_terms( $ad_id, Entities::TAXONOMY_AD_GROUP );
return empty( $terms ) || is_wp_error( $terms )
? [] : wp_list_pluck( $terms, 'term_id' );
* Make sure keys (ad_ids) can be converted to positive integers and weights are integers as well.
* @param array $weights ad weights array with (key: ad id; value: weight).
private function sanitize_ad_weights( array $weights ): array {
foreach ( $weights as $ad_id => $weight ) {
$ad_id_int = absint( $ad_id );
if ( 0 === $ad_id_int || array_key_exists( $ad_id_int, $sanitized_weights ) ) {
$sanitized_weights[ $ad_id_int ] = absint( $weight );
return $sanitized_weights;