: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Get the placements that includes the ad or group.
* @param string $type 'ad' or 'group'.
public static function get_placements_by( $type, $id ) {
$placements = Advanced_Ads::get_ad_placements_array();
foreach ( $placements as $_id => $_placement ) {
if ( isset( $_placement['item'] ) && $_placement['item'] === $type . '_' . $id ) {
$result[ $_id ] = $_placement;
* Get the markup for the group/ad selection for each placement.
* @param string $slug Slug for current placement. This is passed to the view.
* @param array $placement The current placement.
public static function get_items_for_placement_markup( $slug, $placement ) {
$placement['item'] = $placement['item'] ?? '';
// Get the currently selected item.
$placement_item_array = explode( '_', $placement['item'] );
$placement_item_type = $placement_item_array[0];
$placement_item_id = (int) ( $placement_item_array[1] ?? 0 );
$items = self::get_items_for_placement( $placement['type'], $placement['item'] );
// check for missing items
if ( $placement_item_type && ! array_key_exists( $placement['item'], $items[ $placement_item_type . 's' ]['items'] ) ) {
$method = $placement_item_type === 'group' ? 'get_ad_groups' : 'get_ads';
$item = \Advanced_Ads::get_instance()->get_model()->{$method}( [ 'include' => $placement_item_id ] )[0] ?? null;
$items[ $placement_item_type . 's' ]['items'][ $placement['item'] ] = [
if ( $item instanceof WP_Post ) {
$items[ $placement_item_type . 's' ]['items'][ $placement['item'] ]['name'] = $item->post_title;
} elseif ( $item instanceof Advanced_Ads_Group ) {
$items[ $placement_item_type . 's' ]['items'][ $placement['item'] ]['name'] = $item->name;
unset( $items[ $placement_item_type . 's' ]['items'][ $placement['item'] ] );
if ( isset( $items[ $placement_item_type . 's' ]['items'][ $placement['item'] ] ) ) {
$items = array_map( static function( $items_group ) {
$keys = array_column( $items_group['items'], 'name' );
array_multisort( $keys, SORT_ASC, SORT_NATURAL, $items_group['items'] );
$items = array_filter( $items, static function( $items_group ) {
return ! empty( $items_group['items'] );
include ADVADS_ABSPATH . 'admin/views/placements-item.php';
* Get the available items for the selected placement.
* @param string $type The current placement type.
* @param string $item The ad/group id.
public static function get_items_for_placement( string $type, string $item = 'ad_0' ) : iterable {
$placement_type = self::get_placement_types()[ $type ];
'label' => __( 'Ad Groups', 'advanced-ads' ),
'items' => $placement_type->get_allowed_groups(),
'label' => __( 'Ads', 'advanced-ads' ),
'items' => $placement_type->get_allowed_ads(),
return array_map( static function( $items_group ) use ( $item ) {
array_walk( $items_group['items'], static function( &$value, $key ) use ( $item ) {
'selected' => $key === $item,
* Get paths of ancestors that should not contain ads.
* @param object $xpath DOMXPath object.
* @return array Paths of ancestors.
private static function get_ancestors_to_limit( $xpath ) {
$query = self::get_ancestors_to_limit_query();
$node_list = $xpath->query( $query );
$ancestors_to_limit = [];
foreach ( $node_list as $a ) {
$ancestors_to_limit[] = $a->getNodePath();
return $ancestors_to_limit;
* Remove paragraphs that has ancestors that should not contain ads.
* @param array $paragraphs An array of `DOMNode` objects to insert ads before or after.
* @param array $ancestors_to_limit Paths of ancestor that should not contain ads.
* @return array $new_paragraphs An array of `DOMNode` objects to insert ads before or after.
private static function filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit ) {
foreach ( $paragraphs as $k => $paragraph ) {
foreach ( $ancestors_to_limit as $a ) {
if ( 0 === stripos( $paragraph->getNodePath(), $a ) ) {
$new_paragraphs[] = $paragraph;
* Get query to select ancestors that should not contain ads.
* @return string/false DOMXPath query or false.
private static function get_ancestors_to_limit_query() {
* - support `%` (rand) at the start
* - support plain text that node should contain instead of CSS selectors
* - support `prev` and `next` as `type`
* Filter the nodes that limit injection.
* @param array An array of arrays, each of which contains:
* @type string $type Accept: `ancestor` - limit injection inside the ancestor.
* @type string $node A "class selector" which targets one class (.) or "id selector" which targets one id (#),
* optionally with `%` at the end.
'advanced-ads-content-injection-nodes-without-ads',
// a class anyone can use to prevent automatic ad injection into a specific element.
'node' => '.advads-stop-injection',
// Product Slider for Beaver Builder by WooPack.
'node' => '.woopack-product-carousel',
// GeoDirectory Post Slider.
'node' => '.geodir-post-slider',
foreach ( $items as $p ) {
$sel_type = substr( $sel, 0, 1 );
$sel = substr( $sel, 1 );
$rand_pos = strpos( $sel, '%' );
$sel = str_replace( '%', '', $sel );
$sel = sanitize_html_class( $sel );
if ( '.' === $sel_type ) {
if ( false !== $rand_pos ) {
$query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel')";
$query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel ')";
if ( '#' === $sel_type ) {
if ( false !== $rand_pos ) {
$query[] = "@id and starts-with(@id, '$sel')";
$query[] = "@id and @id = '$sel'";
return '//*[' . implode( ' or ', $query ) . ']';
* @param array $placements Existing placements.
* @param string $orderby The field to order by. Accept `name` or `type`.
* @return array $placements Sorted placements.
public static function sort( $placements, $orderby = 'name' ) {
if ( ! is_array( $placements ) ) {
if ( 'name' === $orderby ) {
ksort( $placements, SORT_NATURAL );
uasort( $placements, [ 'Advanced_Ads_Placements', 'sort_by_type_callback' ] );
* Callback to sort placements by type.
* @param array $f First placement.
* @param array $s Second placement.
* @return int 0 If placements are equal, -1 if the first should come first, 1 otherwise.
private static function sort_by_type_callback( $f, $s ) {
// A placement with the "Words Between Ads" option set to non-zero gets injected after others
// because it reads existing ads.
if ( ! empty( $f['options']['words_between_repeats'] ) xor ! empty( $s['options']['words_between_repeats'] ) ) {
return ! empty( $f['options']['words_between_repeats'] ) ? 1 : -1;
$types = self::get_placement_types();
$f_o = ( isset( $f['type'] ) && isset( $types[ $f['type'] ]['order'] ) ) ? $types[ $f['type'] ]['order'] : 100;
$s_o = ( isset( $s['type'] ) && isset( $types[ $s['type'] ]['order'] ) ) ? $types[ $s['type'] ]['order'] : 100;
if ( 'post_content' === $f['type'] && isset( $f['options']['index'] ) && isset( $s['options']['index'] )
&& $f['options']['index'] !== $s['options']['index'] ) {
return ( $f['options']['index'] < $s['options']['index'] ) ? -1 : 1;
if ( isset( $f['name'] ) && isset( $s['name'] ) ) {
return 0 > strnatcmp( $f['name'], $s['name'] ) ? -1 : 1;
return ( $f_o < $s_o ) ? -1 : 1;