PluginProbe ʕ •ᴥ•ʔ
Advanced Ads – Ad Manager & AdSense / 2.0.10
Advanced Ads – Ad Manager & AdSense v2.0.10
2.0.23 2.0.22 2.0.21 1.38.0 1.39.0 1.39.1 1.39.2 1.39.3 1.39.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.40.0 1.40.1 1.40.2 1.41.0 1.42.0 1.42.1 1.42.2 1.43.0 1.44.0 1.44.1 1.45.0 1.45.1 1.46.0 1.47.0 1.47.1 1.47.2 1.47.3 1.47.4 1.47.5 1.48.0 1.48.1 1.49.0 1.5.0 1.5.0.1 1.5.1 1.5.2 1.5.2.1 1.5.4 1.5.4.1 1.5.5 1.50.0 1.51.0 1.51.1 1.51.2 1.51.3 1.52.0 1.52.1 1.52.2 1.52.3 1.52.4 1.53.0 1.53.1 1.53.2 1.54.0 1.54.1 1.55.0 1.56.0 1.56.1 1.56.2 1.56.3 1.56.4 1.6 1.6.1 1.6.10 1.6.10.1 1.6.10.2 1.6.11 1.6.11.1 1.6.12 1.6.13 1.6.14 1.6.15 1.6.16 1.6.17 1.6.17.1 1.6.17.2 1.6.2 1.6.2.1 1.6.3 1.6.4 1.6.4.1 1.6.5 1.6.6 1.6.6.1 1.6.7 1.6.7.1 1.6.8 1.6.8.1 1.6.8.2 1.6.8.3 1.6.9 1.6.9.1 1.6.9.2 1.6.9.3 1.6.9.4 1.7 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.1.1 1.7.1.2 1.7.1.3 1.7.1.4 1.7.1.5 1.7.10 trunk 1.7.11 1.0.1 1.7.12 1.0.2 1.7.13 1.0.3 1.7.14 1.1.0 1.7.15 1.1.1 1.7.16 1.1.2 1.7.17 1.1.3 1.7.18 1.10 1.7.19 1.10.1 1.7.2 1.10.10 1.7.2.1 1.10.11 1.7.20 1.10.12 1.7.21 1.10.2 1.7.22 1.10.3 1.7.23 1.10.4 1.7.24 1.10.5 1.7.25 1.10.6 1.7.3 1.10.7 1.7.4 1.10.8 1.7.4.1 1.10.9 1.7.4.2 1.11 1.7.4.3 1.11.1 1.7.4.4 1.11.2 1.7.4.5 1.12 1.7.5 1.13 1.7.5.1 1.13.1 1.7.6 1.13.2 1.7.7 1.13.3 1.7.8 1.13.4 1.7.9 1.13.5 1.7.9.1 1.13.6 1.7.9.2 1.13.7 1.7.9.3 1.13.8 1.8 1.14 1.8.1 1.14.1 1.8.10 1.14.10 1.8.11 1.14.11 1.8.12 1.14.2 1.8.13 1.14.3 1.8.14 1.14.4 1.8.15 1.14.5 1.8.16 1.14.6 1.8.17 1.14.7 1.8.18 1.14.8 1.8.19 1.14.9 1.8.2 1.15 1.8.20 1.16 1.8.21 1.16.1 1.8.22 1.17 1.8.23 1.17.1 1.8.24 1.17.10 1.8.25 1.17.10-rc.1 1.8.26 1.17.11 1.8.27 1.17.12 1.8.28 1.17.12-rc.1 1.8.29 1.17.2 1.8.3 1.17.3 1.8.30 1.17.4 1.8.4 1.17.5 1.8.5 1.17.6 1.8.6 1.17.7 1.8.7 1.17.8 1.8.8 1.17.9 1.8.9 1.17.9-beta.1 1.9 1.18.0 2.0.0 1.19.0 2.0.1 1.19.1 2.0.10 1.2 2.0.11 1.2.1 2.0.12 1.2.2 2.0.13 1.2.3 2.0.14 1.2.4 2.0.15 1.2.5 2.0.16 1.2.6 2.0.17 1.2.7 2.0.18 1.20.0 2.0.19 1.20.0-rc.1 2.0.2 1.20.0-rc.2 2.0.20 1.20.1 2.0.3 1.20.2 2.0.4 1.20.3 2.0.5 1.21.0 2.0.6 1.21.1 2.0.7 1.22.0 2.0.8 1.22.1 2.0.9 1.22.2 1.23.0 1.23.1 1.23.2 1.24.0 1.24.1 1.24.2 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.29.0 1.29.1 1.3 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 1.3.18 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.30.0 1.30.1 1.30.2 1.30.2-rc.1 1.30.3 1.30.4 1.30.4-rc.1 1.30.5 1.31.0 1.31.1 1.32.0 1.32.0-rc.1 1.33.0 1.33.1 1.33.2 1.34.0 1.35.0 1.35.1 1.36.0 1.36.1 1.36.2 1.36.3 1.37.0 1.37.1 1.37.2
advanced-ads / classes / in-content-injector.php
advanced-ads / classes Last commit date
ad-health-notices.php 1 year ago checks.php 1 year ago display-conditions.php 11 months ago filesystem.php 2 years ago frontend_checks.php 1 year ago in-content-injector.php 1 year ago inline-css.php 1 year ago utils.php 1 year ago visitor-conditions.php 1 year ago
in-content-injector.php
759 lines
1 <?php // phpcs:ignoreFileName
2
3 use AdvancedAds\Utilities\Conditional;
4
5 /**
6 * Injects ads in the content based on an XPath expression.
7 */
8 class Advanced_Ads_In_Content_Injector {
9
10 /**
11 * Gather placeholders which later are replaced by the ads
12 *
13 * @var array $ads_for_placeholders
14 */
15 private static $ads_for_placeholders = [];
16
17 /**
18 * Inject ads directly into the content
19 *
20 * @param string $placement_id Id of the placement.
21 * @param array $placement_opts Placement options.
22 * @param string $content Content to inject placement into.
23 * @param array $options {
24 * Injection options.
25 *
26 * @type bool $allowEmpty Whether the tag can be empty to be counted.
27 * @type bool $paragraph_select_from_bottom Whether to select ads from buttom.
28 * @type string $position Position. Can be one of 'before', 'after', 'append', 'prepend'
29 * @type number $alter_nodes Whether to alter nodes, for example to prevent injecting ads into `a` tags.
30 * @type bool $repeat Whether to repeat the position.
31 * @type number $paragraph_id Paragraph Id.
32 * @type number $itemLimit If there are too few items at this level test nesting. Set to '-1` to prevent testing.
33 * }
34 *
35 * @return string $content Content with injected placement.
36 */
37 public static function &inject_in_content( $placement_id, $placement_opts, &$content, $options = [] ) {
38 if ( ! extension_loaded( 'dom' ) ) {
39 return $content;
40 }
41
42 // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
43 // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
44
45 // parse arguments.
46 $tag = isset( $placement_opts['tag'] ) ? $placement_opts['tag'] : 'p';
47 $tag = preg_replace( '/[^a-z0-9]/i', '', $tag ); // simplify tag.
48 /**
49 * Store the original tag value since $tag is changed on the fly and we might want to know the original selected
50 * options for some checks later.
51 */
52 $tag_option = $tag;
53
54 // allow more complex xPath expression.
55 $tag = apply_filters( 'advanced-ads-placement-content-injection-xpath', $tag, $placement_opts );
56
57 // get plugin options.
58 $plugin_options = Advanced_Ads::get_instance()->options();
59
60 $defaults = [
61 'allowEmpty' => false,
62 'paragraph_select_from_bottom' => isset( $placement_opts['start_from_bottom'] ) && $placement_opts['start_from_bottom'],
63 'position' => isset( $placement_opts['position'] ) ? $placement_opts['position'] : 'after',
64 // only has before and after.
65 'before' => isset( $placement_opts['position'] ) && 'before' === $placement_opts['position'],
66 // Whether to alter nodes, for example to prevent injecting ads into `a` tags.
67 'alter_nodes' => true,
68 'repeat' => false,
69 ];
70
71 $defaults['paragraph_id'] = isset( $placement_opts['index'] ) ? $placement_opts['index'] : 1;
72 $defaults['paragraph_id'] = max( 1, (int) $defaults['paragraph_id'] );
73
74 // if there are too few items at this level test nesting.
75 $defaults['itemLimit'] = 'p' === $tag_option ? 2 : 1;
76
77 // trigger such a high item limit that all elements will be considered.
78 if ( ! empty( $plugin_options['content-injection-level-disabled'] ) ) {
79 $defaults['itemLimit'] = 1000;
80 }
81
82 // Handle tags that are empty by definition or could be empty ("custom" option).
83 if ( in_array( $tag_option, [ 'img', 'iframe', 'custom' ], true ) ) {
84 $defaults['allowEmpty'] = true;
85 }
86
87 // Merge the options if possible. If there are common keys, we don't merge them to prevent overriding and unexpected behavior.
88 $common_keys = array_intersect_key( $options, $placement_opts );
89 if ( empty( $common_keys ) ) {
90 $options = array_merge( $options, $placement_opts );
91 }
92
93 // allow hooks to change some options.
94 $options = apply_filters(
95 'advanced-ads-placement-content-injection-options',
96 wp_parse_args( $options, $defaults ),
97 $tag_option
98 );
99
100 $wp_charset = get_bloginfo( 'charset' );
101 // parse document as DOM (fragment - having only a part of an actual post given).
102
103 $content_to_load = self::get_content_to_load( $content );
104 if ( ! $content_to_load ) {
105 return $content;
106 }
107
108 $dom = new DOMDocument( '1.0', $wp_charset );
109 // may loose some fragments or add autop-like code.
110 $libxml_use_internal_errors = libxml_use_internal_errors( true ); // avoid notices and warnings - html is most likely malformed.
111
112 $success = $dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $content_to_load );
113 libxml_use_internal_errors( $libxml_use_internal_errors );
114 if ( true !== $success ) {
115 // -TODO handle cases were dom-parsing failed (at least inform user)
116 return $content;
117 }
118
119 /**
120 * Handle advanced tags.
121 */
122 switch ( $tag_option ) {
123 case 'p':
124 // Exclude paragraphs within blockquote tags.
125 $tag = 'p[not(parent::blockquote)]';
126 break;
127 case 'pwithoutimg':
128 // Convert option name into correct path, exclude paragraphs within blockquote tags.
129 $tag = 'p[not(descendant::img) and not(parent::blockquote)]';
130 break;
131 case 'img':
132 /*
133 * Handle: 1) "img" tags 2) "image" block 3) "gallery" block 4) "gallery shortcode" 5) "wp_caption" shortcode
134 * Handle the gallery created by the block or the shortcode as one image.
135 * Prevent injection of ads next to images in tables.
136 */
137 // Default shortcodes, including non-HTML5 versions.
138 $shortcodes = "@class and (
139 contains(concat(' ', normalize-space(@class), ' '), ' gallery-size') or
140 contains(concat(' ', normalize-space(@class), ' '), ' wp-caption ') )";
141 $tag = "*[self::img or self::figure or self::div[$shortcodes]]
142 [not(ancestor::table or ancestor::figure or ancestor::div[$shortcodes])]";
143 break;
144 // Any headline. By default h2, h3, and h4.
145 case 'headlines':
146 $headlines = apply_filters( 'advanced-ads-headlines-for-ad-injection', [ 'h2', 'h3', 'h4' ] );
147
148 foreach ( $headlines as &$headline ) {
149 $headline = 'self::' . $headline;
150 }
151 $tag = '*[' . implode( ' or ', $headlines ) . ']'; // /html/body/*[self::h2 or self::h3 or self::h4]
152 break;
153 // Any HTML element that makes sense in the content.
154 case 'anyelement':
155 $exclude = [
156 'html',
157 'body',
158 'script',
159 'style',
160 'tr',
161 'td',
162 // Inline tags.
163 'a',
164 'abbr',
165 'b',
166 'bdo',
167 'br',
168 'button',
169 'cite',
170 'code',
171 'dfn',
172 'em',
173 'i',
174 'img',
175 'kbd',
176 'label',
177 'option',
178 'q',
179 'samp',
180 'select',
181 'small',
182 'span',
183 'strong',
184 'sub',
185 'sup',
186 'textarea',
187 'time',
188 'tt',
189 'var',
190 ];
191 $tag = '*[not(self::' . implode( ' or self::', $exclude ) . ')]';
192 break;
193 case 'custom':
194 // Get the path for the "custom" tag choice, use p as a fallback to prevent it from showing any ads if users left it empty.
195 $tag = ! empty( $placement_opts['xpath'] ) ? stripslashes( $placement_opts['xpath'] ) : 'p';
196 break;
197 }
198
199 // select positions.
200 $xpath = new DOMXPath( $dom );
201
202 if ( -1 !== $options['itemLimit'] ) {
203 $items = $xpath->query( '/html/body/' . $tag );
204
205 if ( $items->length < $options['itemLimit'] ) {
206 $items = $xpath->query( '/html/body/*/' . $tag );
207 }
208 // try third level.
209 if ( $items->length < $options['itemLimit'] ) {
210 $items = $xpath->query( '/html/body/*/*/' . $tag );
211 }
212 // try all levels as last resort.
213 if ( $items->length < $options['itemLimit'] ) {
214 $items = $xpath->query( '//' . $tag );
215 }
216 } else {
217 $items = $xpath->query( $tag );
218 }
219
220 // allow to select other elements.
221 $items = apply_filters( 'advanced-ads-placement-content-injection-items', $items, $xpath, $tag_option );
222
223 // filter empty tags from items.
224 $whitespaces = json_decode( '"\t\n\r \u00A0"' );
225 $paragraphs = [];
226 foreach ( $items as $item ) {
227 if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
228 $paragraphs[] = $item;
229 }
230 }
231
232 $ancestors_to_limit = self::get_ancestors_to_limit( $xpath );
233 $paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
234
235 $options['paragraph_count'] = count( $paragraphs );
236
237 if ( $options['paragraph_count'] >= $options['paragraph_id'] ) {
238 $offset = $options['paragraph_select_from_bottom'] ? $options['paragraph_count'] - $options['paragraph_id'] : $options['paragraph_id'] - 1;
239 $offsets = apply_filters( 'advanced-ads-placement-content-offsets', [ $offset ], $options, $placement_opts, $xpath, $paragraphs, $dom );
240 $did_inject = false;
241
242 foreach ( $offsets as $offset ) {
243
244 // inject.
245 $node = apply_filters( 'advanced-ads-placement-content-injection-node', $paragraphs[ $offset ], $tag, $options['before'] );
246
247 if ( $options['alter_nodes'] ) {
248 // Prevent injection into image caption and gallery.
249 $parent = $node;
250 for ( $i = 0; $i < 4; $i++ ) {
251 $parent = $parent->parentNode;
252 if ( ! $parent instanceof DOMElement ) {
253 break;
254 }
255 if ( preg_match( '/\b(wp-caption|gallery-size)\b/', $parent->getAttribute( 'class' ) ) ) {
256 $node = $parent;
257 break;
258 }
259 }
260
261 // Make sure that the ad is injected outside the link.
262 if ( 'img' === $tag_option && 'a' === $node->parentNode->tagName ) {
263 if ( $options['before'] ) {
264 $node->parentNode;
265 } else {
266 // Go one level deeper if inserted after to not insert the ad into the link; probably after the paragraph.
267 $node->parentNode->parentNode;
268 }
269 }
270 }
271
272 $ad_content = (string) get_the_placement( $placement_id, '', $placement_opts );
273
274 if ( trim( $ad_content, $whitespaces ) === '' ) {
275 continue;
276 }
277
278 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
279 $ad_content = self::filter_ad_content( $ad_content, $node->tagName, $options );
280
281 // convert HTML to XML!
282 $ad_dom = new DOMDocument( '1.0', $wp_charset );
283 $libxml_use_internal_errors = libxml_use_internal_errors( true );
284 $ad_dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $ad_content );
285
286 switch ( $options['position'] ) {
287 case 'append':
288 $ref_node = $node;
289
290 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
291 $importedNode = $dom->importNode( $importedNode, true );
292 $ref_node->appendChild( $importedNode );
293 }
294 break;
295 case 'prepend':
296 $ref_node = $node;
297
298 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
299 $importedNode = $dom->importNode( $importedNode, true );
300 $ref_node->insertBefore( $importedNode, $ref_node->firstChild );
301 }
302 break;
303 case 'before':
304 $ref_node = $node;
305
306 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
307 $importedNode = $dom->importNode( $importedNode, true );
308 $ref_node->parentNode->insertBefore( $importedNode, $ref_node );
309 }
310 break;
311 case 'after':
312 default:
313 // append before next node or as last child to body.
314 $ref_node = $node->nextSibling;
315 if ( isset( $ref_node ) ) {
316 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
317 $importedNode = $dom->importNode( $importedNode, true );
318 $ref_node->parentNode->insertBefore( $importedNode, $ref_node );
319 }
320 } else {
321 // append to body; -TODO using here that we only select direct children of the body tag.
322 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
323 $importedNode = $dom->importNode( $importedNode, true );
324 $node->parentNode->appendChild( $importedNode );
325 }
326 }
327 }
328 libxml_use_internal_errors( $libxml_use_internal_errors );
329 $did_inject = true;
330 }
331
332 if ( ! $did_inject ) {
333 return $content;
334 }
335
336 $content_orig = $content;
337 // convert to text-representation.
338 $content = $dom->saveHTML();
339 $content = self::prepare_output( $content, $content_orig );
340
341 /**
342 * Show a warning to ad admins in the Ad Health bar in the frontend, when
343 *
344 * * the level limitation was not disabled
345 * * could not inject one ad (as by use of `elseif` here)
346 * * but there are enough elements on the site, but just in sub-containers
347 */
348 } elseif ( Conditional::user_can( 'advanced_ads_manage_options' )
349 && -1 !== $options['itemLimit']
350 && empty( $plugin_options['content-injection-level-disabled'] ) ) {
351
352 // Check if there are more elements without limitation.
353 $all_items = $xpath->query( '//' . $tag );
354
355 $paragraphs = [];
356 foreach ( $all_items as $item ) {
357 if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
358 $paragraphs[] = $item;
359 }
360 }
361
362 $paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
363 if ( $options['paragraph_id'] <= count( $paragraphs ) ) {
364 // Add a warning to ad health.
365 add_filter( 'advanced-ads-ad-health-nodes', [ 'Advanced_Ads_In_Content_Injector', 'add_ad_health_node' ] );
366 }
367 }
368
369 // phpcs:enable
370
371 return $content;
372 }
373
374 /**
375 * Get content to load.
376 *
377 * @param string $content Original content.
378 *
379 * @return string $content Content to load.
380 */
381 private static function get_content_to_load( $content ) {
382 // Prevent removing closing tags in scripts.
383 $content_to_load = preg_replace( '/<script.*?<\/script>/si', '<!--\0-->', $content );
384
385 // check which priority the wpautop filter has; might have been disabled on purpose.
386 $wpautop_priority = has_filter( 'the_content', 'wpautop' );
387 if ( $wpautop_priority && Advanced_Ads::get_instance()->get_content_injection_priority() < $wpautop_priority ) {
388 $content_to_load = wpautop( $content_to_load );
389 }
390
391 return $content_to_load;
392 }
393
394 /**
395 * Filter ad content.
396 *
397 * @param string $ad_content Ad content.
398 * @param string $tag_name tar before/after the content.
399 * @param array $options Injection options.
400 *
401 * @return string ad content.
402 */
403 private static function filter_ad_content( $ad_content, $tag_name, $options ) {
404 // Replace `</` with `<\/` in ad content when placed within `document.write()` to prevent code from breaking.
405 $ad_content = preg_replace( '#(document.write.+)</(.*)#', '$1<\/$2', $ad_content );
406
407 // Inject placeholder.
408 $id = count( self::$ads_for_placeholders );
409 self::$ads_for_placeholders[] = [
410 'id' => $id,
411 'tag' => $tag_name,
412 'position' => $options['position'],
413 'ad' => $ad_content,
414 ];
415
416 return '%advads_placeholder_' . $id . '%';
417 }
418
419 /**
420 * Prepare output.
421 *
422 * @param string $content Modified content.
423 * @param string $content_orig Original content.
424 *
425 * @return string $content Content to output.
426 */
427 private static function prepare_output( $content, $content_orig ) {
428 $content = self::inject_ads( $content, $content_orig, self::$ads_for_placeholders );
429 self::$ads_for_placeholders = [];
430
431 return $content;
432 }
433
434 /**
435 * Search for ad placeholders in the `$content` to determine positions at which to inject ads.
436 * Given the positions, inject ads into `$content_orig.
437 *
438 * @param string $content Post content with injected ad placeholders.
439 * @param string $content_orig Unmodified post content.
440 * @param array $ads_for_placeholders Array of ads.
441 * Each ad contains placeholder id, before or after which tag to inject the ad, the ad content.
442 *
443 * @return string $content
444 */
445 private static function inject_ads( $content, $content_orig, $ads_for_placeholders ) {
446 $self_closing_tags = [
447 'area',
448 'base',
449 'basefont',
450 'bgsound',
451 'br',
452 'col',
453 'embed',
454 'frame',
455 'hr',
456 'img',
457 'input',
458 'keygen',
459 'link',
460 'meta',
461 'param',
462 'source',
463 'track',
464 'wbr',
465 ];
466
467 // It is not possible to append/prepend in self closing tags.
468 foreach ( $ads_for_placeholders as &$ad_content ) {
469 if (
470 ( 'prepend' === $ad_content['position'] || 'append' === $ad_content['position'] ) &&
471 in_array( $ad_content['tag'], $self_closing_tags, true )
472 ) {
473 $ad_content['position'] = 'after';
474 }
475 }
476 unset( $ad_content );
477 usort( $ads_for_placeholders, [ 'Advanced_Ads_In_Content_Injector', 'sort_ads_for_placehoders' ] );
478
479 // Add tags before/after which ad placehoders were injected.
480 $alts = [];
481 foreach ( $ads_for_placeholders as $ad_content ) {
482 $tag = $ad_content['tag'];
483
484 switch ( $ad_content['position'] ) {
485 case 'before':
486 case 'prepend':
487 $alts[] = "<{$tag}[^>]*>";
488 break;
489 case 'after':
490 if ( in_array( $tag, $self_closing_tags, true ) ) {
491 $alts[] = "<{$tag}[^>]*>";
492 } else {
493 $alts[] = "</{$tag}>";
494 }
495 break;
496 case 'append':
497 $alts[] = "</{$tag}>";
498 break;
499 }
500 }
501 $alts = array_unique( $alts );
502 $tag_regexp = implode( '|', $alts );
503 // Add ad placeholder.
504 $alts[] = '%advads_placeholder_(?:\d+)%';
505 $tag_and_placeholder_regexp = implode( '|', $alts );
506
507 preg_match_all( "#{$tag_and_placeholder_regexp}#i", $content, $tag_matches );
508 $count = 0;
509
510 // For each tag located before/after an ad placeholder, find its offset among the same tags.
511 foreach ( $tag_matches[0] as $r ) {
512 if ( preg_match( '/%advads_placeholder_(\d+)%/', $r, $result ) ) {
513 $id = $result[1];
514 $found_ad = false;
515 foreach ( $ads_for_placeholders as $n => $ad ) {
516 if ( (int) $ad['id'] === (int) $id ) {
517 $found_ad = $ad;
518 break;
519 }
520 }
521 if ( ! $found_ad ) {
522 continue;
523 }
524
525 switch ( $found_ad['position'] ) {
526 case 'before':
527 case 'append':
528 $ads_for_placeholders[ $n ]['offset'] = $count;
529 break;
530 case 'after':
531 case 'prepend':
532 $ads_for_placeholders[ $n ]['offset'] = $count - 1;
533 break;
534 }
535 } else {
536 ++$count;
537 }
538 }
539
540 // Find tags before/after which we need to inject ads.
541 preg_match_all( "#{$tag_regexp}#i", $content_orig, $orig_tag_matches, PREG_OFFSET_CAPTURE );
542 $new_content = '';
543 $pos = 0;
544
545 foreach ( $orig_tag_matches[0] as $n => $r ) {
546 $to_inject = [];
547 // Check if we need to inject an ad at this offset.
548 foreach ( $ads_for_placeholders as $ad ) {
549 if ( isset( $ad['offset'] ) && $ad['offset'] === $n ) {
550 $to_inject[] = $ad;
551 }
552 }
553
554 foreach ( $to_inject as $item ) {
555 switch ( $item['position'] ) {
556 case 'before':
557 case 'append':
558 $found_pos = $r[1];
559 break;
560 case 'after':
561 case 'prepend':
562 $found_pos = $r[1] + strlen( $r[0] );
563 break;
564 }
565
566 $new_content .= substr( $content_orig, $pos, $found_pos - $pos );
567 $pos = $found_pos;
568 $new_content .= $item['ad'];
569 }
570 }
571 $new_content .= substr( $content_orig, $pos );
572
573 return $new_content;
574 }
575
576
577 /**
578 * Callback function for usort() to sort ads for placeholders.
579 *
580 * @param array $first The first array to compare.
581 * @param array $second The second array to compare.
582 *
583 * @return int 0 if both objects equal. -1 if second array should come first, 1 otherwise.
584 */
585 public static function sort_ads_for_placehoders( $first, $second ) {
586 if ( $first['position'] === $second['position'] ) {
587 return 0;
588 }
589
590 $num = [
591 'before' => 1,
592 'prepend' => 2,
593 'append' => 3,
594 'after' => 4,
595 ];
596
597 return $num[ $first['position'] ] > $num[ $second['position'] ] ? 1 : - 1;
598 }
599
600 /**
601 * Add a warning to 'Ad health'.
602 *
603 * @param array $nodes .
604 *
605 * @return array $nodes.
606 */
607 public static function add_ad_health_node( $nodes ) {
608 $nodes[] = [
609 'type' => 1,
610 'data' => [
611 'parent' => 'advanced_ads_ad_health',
612 'id' => 'advanced_ads_ad_health_the_content_not_enough_elements',
613 'title' => sprintf(
614 /* translators: %s stands for the name of the "Disable level limitation" option and automatically translated as well */
615 __( 'Set <em>%s</em> to show more ads', 'advanced-ads' ),
616 __( 'Disable level limitation', 'advanced-ads' )
617 ),
618 'href' => admin_url( '/admin.php?page=advanced-ads-settings#top#general' ),
619 'meta' => [
620 'class' => 'advanced_ads_ad_health_warning',
621 'target' => '_blank',
622 ],
623 ],
624 ];
625
626 return $nodes;
627 }
628
629 /**
630 * Get paths of ancestors that should not contain ads.
631 *
632 * @param object $xpath DOMXPath object.
633 *
634 * @return array Paths of ancestors.
635 */
636 private static function get_ancestors_to_limit( $xpath ) {
637 $query = self::get_ancestors_to_limit_query();
638 if ( ! $query ) {
639 return [];
640 }
641
642 $node_list = $xpath->query( $query );
643 $ancestors_to_limit = [];
644
645 foreach ( $node_list as $a ) {
646 $ancestors_to_limit[] = $a->getNodePath();
647 }
648
649 return $ancestors_to_limit;
650 }
651
652
653 /**
654 * Remove paragraphs that has ancestors that should not contain ads.
655 *
656 * @param array $paragraphs An array of `DOMNode` objects to insert ads before or after.
657 * @param array $ancestors_to_limit Paths of ancestor that should not contain ads.
658 *
659 * @return array $new_paragraphs An array of `DOMNode` objects to insert ads before or after.
660 */
661 private static function filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit ) {
662 $new_paragraphs = [];
663
664 foreach ( $paragraphs as $k => $paragraph ) {
665 foreach ( $ancestors_to_limit as $a ) {
666 if ( 0 === stripos( $paragraph->getNodePath(), $a ) ) {
667 continue 2;
668 }
669 }
670
671 $new_paragraphs[] = $paragraph;
672 }
673
674 return $new_paragraphs;
675 }
676
677 /**
678 * Get query to select ancestors that should not contain ads.
679 *
680 * @return string/false DOMXPath query or false.
681 */
682 private static function get_ancestors_to_limit_query() {
683 /**
684 * TODO:
685 * - support `%` (rand) at the start
686 * - support plain text that node should contain instead of CSS selectors
687 * - support `prev` and `next` as `type`
688 */
689
690 /**
691 * Filter the nodes that limit injection.
692 *
693 * @param array An array of arrays, each of which contains:
694 *
695 * @type string $type Accept: `ancestor` - limit injection inside the ancestor.
696 * @type string $node A "class selector" which targets one class (.) or "id selector" which targets one id (#),
697 * optionally with `%` at the end.
698 */
699 $items = apply_filters(
700 'advanced-ads-content-injection-nodes-without-ads',
701 [
702 [
703 // a class anyone can use to prevent automatic ad injection into a specific element.
704 'node' => '.advads-stop-injection',
705 'type' => 'ancestor',
706 ],
707 [
708 // Product Slider for Beaver Builder by WooPack.
709 'node' => '.woopack-product-carousel',
710 'type' => 'ancestor',
711 ],
712 [
713 // WP Author Box Lite.
714 'node' => '#wpautbox-%',
715 'type' => 'ancestor',
716 ],
717 [
718 // GeoDirectory Post Slider.
719 'node' => '.geodir-post-slider',
720 'type' => 'ancestor',
721 ],
722 ]
723 );
724
725 $query = [];
726 foreach ( $items as $p ) {
727 $sel = $p['node'];
728
729 $sel_type = substr( $sel, 0, 1 );
730 $sel = substr( $sel, 1 );
731
732 $rand_pos = strpos( $sel, '%' );
733 $sel = str_replace( '%', '', $sel );
734 $sel = sanitize_html_class( $sel );
735
736 if ( '.' === $sel_type ) {
737 if ( false !== $rand_pos ) {
738 $query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel')";
739 } else {
740 $query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel ')";
741 }
742 }
743 if ( '#' === $sel_type ) {
744 if ( false !== $rand_pos ) {
745 $query[] = "@id and starts-with(@id, '$sel')";
746 } else {
747 $query[] = "@id and @id = '$sel'";
748 }
749 }
750 }
751
752 if ( ! $query ) {
753 return false;
754 }
755
756 return '//*[' . implode( ' or ', $query ) . ']';
757 }
758 }
759