PluginProbe ʕ •ᴥ•ʔ
Advanced Ads – Ad Manager & AdSense / 1.51.3
Advanced Ads – Ad Manager & AdSense v1.51.3
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
Advanced_Ads_Modal.php 2 years ago EDD_SL_Plugin_Updater.php 2 years ago ad-ajax.php 2 years ago ad-debug.php 2 years ago ad-expiration.php 3 years ago ad-health-notices.php 2 years ago ad-model.php 2 years ago ad-select.php 3 years ago ad.php 2 years ago ad_ajax_callbacks.php 2 years ago ad_group.php 2 years ago ad_placements.php 2 years ago ad_type_abstract.php 2 years ago ad_type_content.php 2 years ago ad_type_dummy.php 2 years ago ad_type_group.php 2 years ago ad_type_image.php 2 years ago ad_type_plain.php 2 years ago checks.php 2 years ago class-translation-promo.php 2 years ago compatibility.php 2 years ago display-conditions.php 2 years ago filesystem.php 2 years ago frontend_checks.php 2 years ago in-content-injector.php 2 years ago inline-css.php 2 years ago plugin.php 2 years ago upgrades.php 2 years ago utils.php 3 years ago visitor-conditions.php 2 years ago widget.php 2 years ago
in-content-injector.php
753 lines
1 <?php
2
3 use AdvancedAds\Utilities\WordPress;
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 // allow hooks to change some options.
88 $options = apply_filters(
89 'advanced-ads-placement-content-injection-options',
90 wp_parse_args( $options, $defaults ),
91 $tag_option
92 );
93
94 $wp_charset = get_bloginfo( 'charset' );
95 // parse document as DOM (fragment - having only a part of an actual post given).
96
97 $content_to_load = self::get_content_to_load( $content, $wp_charset );
98 if ( ! $content_to_load ) {
99 return $content;
100 }
101
102 $dom = new DOMDocument( '1.0', $wp_charset );
103 // may loose some fragments or add autop-like code.
104 $libxml_use_internal_errors = libxml_use_internal_errors( true ); // avoid notices and warnings - html is most likely malformed.
105
106 $success = $dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $content_to_load );
107 libxml_use_internal_errors( $libxml_use_internal_errors );
108 if ( true !== $success ) {
109 // -TODO handle cases were dom-parsing failed (at least inform user)
110 return $content;
111 }
112
113 /**
114 * Handle advanced tags.
115 */
116 switch ( $tag_option ) {
117 case 'p':
118 // exclude paragraphs within blockquote tags
119 $tag = 'p[not(parent::blockquote)]';
120 break;
121 case 'pwithoutimg':
122 // convert option name into correct path, exclude paragraphs within blockquote tags
123 $tag = 'p[not(descendant::img) and not(parent::blockquote)]';
124 break;
125 case 'img':
126 /*
127 * Handle: 1) "img" tags 2) "image" block 3) "gallery" block 4) "gallery shortcode" 5) "wp_caption" shortcode
128 * Handle the gallery created by the block or the shortcode as one image.
129 * Prevent injection of ads next to images in tables.
130 */
131 // Default shortcodes, including non-HTML5 versions.
132 $shortcodes = "@class and (
133 contains(concat(' ', normalize-space(@class), ' '), ' gallery-size') or
134 contains(concat(' ', normalize-space(@class), ' '), ' wp-caption ') )";
135 $tag = "*[self::img or self::figure or self::div[$shortcodes]]
136 [not(ancestor::table or ancestor::figure or ancestor::div[$shortcodes])]";
137 break;
138 // any headline. By default h2, h3, and h4
139 case 'headlines':
140 $headlines = apply_filters( 'advanced-ads-headlines-for-ad-injection', [ 'h2', 'h3', 'h4' ] );
141
142 foreach ( $headlines as &$headline ) {
143 $headline = 'self::' . $headline;
144 }
145 $tag = '*[' . implode( ' or ', $headlines ) . ']'; // /html/body/*[self::h2 or self::h3 or self::h4]
146 break;
147 // any HTML element that makes sense in the content
148 case 'anyelement':
149 $exclude = [
150 'html',
151 'body',
152 'script',
153 'style',
154 'tr',
155 'td',
156 // Inline tags.
157 'a',
158 'abbr',
159 'b',
160 'bdo',
161 'br',
162 'button',
163 'cite',
164 'code',
165 'dfn',
166 'em',
167 'i',
168 'img',
169 'kbd',
170 'label',
171 'option',
172 'q',
173 'samp',
174 'select',
175 'small',
176 'span',
177 'strong',
178 'sub',
179 'sup',
180 'textarea',
181 'time',
182 'tt',
183 'var',
184 ];
185 $tag = '*[not(self::' . implode( ' or self::', $exclude ) . ')]';
186 break;
187 case 'custom':
188 // 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
189 $tag = ! empty( $placement_opts['xpath'] ) ? stripslashes( $placement_opts['xpath'] ) : 'p';
190 break;
191 }
192
193 // select positions.
194 $xpath = new DOMXPath( $dom );
195
196
197 if ( $options['itemLimit'] !== -1 ) {
198 $items = $xpath->query( '/html/body/' . $tag );
199
200 if ( $items->length < $options['itemLimit'] ) {
201 $items = $xpath->query( '/html/body/*/' . $tag );
202 }
203 // try third level.
204 if ( $items->length < $options['itemLimit'] ) {
205 $items = $xpath->query( '/html/body/*/*/' . $tag );
206 }
207 // try all levels as last resort.
208 if ( $items->length < $options['itemLimit'] ) {
209 $items = $xpath->query( '//' . $tag );
210 }
211 } else {
212 $items = $xpath->query( $tag );
213 }
214
215 // allow to select other elements.
216 $items = apply_filters( 'advanced-ads-placement-content-injection-items', $items, $xpath, $tag_option );
217
218 // filter empty tags from items.
219 $whitespaces = json_decode( '"\t\n\r \u00A0"' );
220 $paragraphs = [];
221 foreach ( $items as $item ) {
222 if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
223 $paragraphs[] = $item;
224 }
225 }
226
227 $ancestors_to_limit = self::get_ancestors_to_limit( $xpath );
228 $paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
229
230 $options['paragraph_count'] = count( $paragraphs );
231
232 if ( $options['paragraph_count'] >= $options['paragraph_id'] ) {
233 $offset = $options['paragraph_select_from_bottom'] ? $options['paragraph_count'] - $options['paragraph_id'] : $options['paragraph_id'] - 1;
234 $offsets = apply_filters( 'advanced-ads-placement-content-offsets', [ $offset ], $options, $placement_opts, $xpath, $paragraphs, $dom );
235 $did_inject = false;
236
237 foreach ( $offsets as $offset ) {
238
239 // inject.
240 $node = apply_filters( 'advanced-ads-placement-content-injection-node', $paragraphs[ $offset ], $tag, $options['before'] );
241
242 if ( $options['alter_nodes'] ) {
243 // Prevent injection into image caption and gallery.
244 $parent = $node;
245 for ( $i = 0; $i < 4; $i++ ) {
246 $parent = $parent->parentNode;
247 if ( ! $parent instanceof DOMElement ) {
248 break;
249 }
250 if ( preg_match( '/\b(wp-caption|gallery-size)\b/', $parent->getAttribute( 'class' ) ) ) {
251 $node = $parent;
252 break;
253 }
254 }
255
256 // make sure that the ad is injected outside the link
257 if ( 'img' === $tag_option && 'a' === $node->parentNode->tagName ) {
258 if ( $options['before'] ) {
259 $node->parentNode;
260 } else {
261 // go one level deeper if inserted after to not insert the ad into the link; probably after the paragraph
262 $node->parentNode->parentNode;
263 }
264 }
265 }
266
267 $ad_content = (string) Advanced_Ads_Select::get_instance()->get_ad_by_method( $placement_id, 'placement', $placement_opts );
268
269 if ( trim( $ad_content, $whitespaces ) === '' ) {
270 continue;
271 }
272
273 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
274 $ad_content = self::filter_ad_content( $ad_content, $node->tagName, $options );
275
276 // convert HTML to XML!
277 $ad_dom = new DOMDocument( '1.0', $wp_charset );
278 $libxml_use_internal_errors = libxml_use_internal_errors( true );
279 $ad_dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $ad_content );
280
281 switch ( $options['position'] ) {
282 case 'append':
283 $ref_node = $node;
284
285 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
286 $importedNode = $dom->importNode( $importedNode, true );
287 $ref_node->appendChild( $importedNode );
288 }
289 break;
290 case 'prepend':
291 $ref_node = $node;
292
293 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
294 $importedNode = $dom->importNode( $importedNode, true );
295 $ref_node->insertBefore( $importedNode, $ref_node->firstChild );
296 }
297 break;
298 case 'before':
299 $ref_node = $node;
300
301 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
302 $importedNode = $dom->importNode( $importedNode, true );
303 $ref_node->parentNode->insertBefore( $importedNode, $ref_node );
304 }
305 break;
306 case 'after':
307 default:
308 // append before next node or as last child to body.
309 $ref_node = $node->nextSibling;
310 if ( isset( $ref_node ) ) {
311 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
312 $importedNode = $dom->importNode( $importedNode, true );
313 $ref_node->parentNode->insertBefore( $importedNode, $ref_node );
314 }
315 } else {
316 // append to body; -TODO using here that we only select direct children of the body tag.
317 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
318 $importedNode = $dom->importNode( $importedNode, true );
319 $node->parentNode->appendChild( $importedNode );
320 }
321 }
322 }
323 libxml_use_internal_errors( $libxml_use_internal_errors );
324 $did_inject = true;
325 }
326
327 if ( ! $did_inject ) {
328 return $content;
329 }
330
331 $content_orig = $content;
332 // convert to text-representation.
333 $content = $dom->saveHTML();
334 $content = self::prepare_output( $content, $content_orig );
335
336 /**
337 * Show a warning to ad admins in the Ad Health bar in the frontend, when
338 *
339 * * the level limitation was not disabled
340 * * could not inject one ad (as by use of `elseif` here)
341 * * but there are enough elements on the site, but just in sub-containers
342 */
343 } elseif ( WordPress::user_can( 'advanced_ads_manage_options' )
344 && $options['itemLimit'] !== -1
345 && empty( $plugin_options['content-injection-level-disabled'] ) ) {
346
347 // Check if there are more elements without limitation.
348 $all_items = $xpath->query( '//' . $tag );
349
350 $paragraphs = [];
351 foreach ( $all_items as $item ) {
352 if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
353 $paragraphs[] = $item;
354 }
355 }
356
357 $paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
358 if ( $options['paragraph_id'] <= count( $paragraphs ) ) {
359 // Add a warning to ad health.
360 add_filter( 'advanced-ads-ad-health-nodes', [ 'Advanced_Ads_In_Content_Injector', 'add_ad_health_node' ] );
361 }
362 }
363
364 // phpcs:enable
365
366 return $content;
367 }
368
369 /**
370 * Get content to load.
371 *
372 * @param string $content Original content.
373 * @param string $wp_charset blog charset.
374 *
375 * @return string $content Content to load.
376 */
377 private static function get_content_to_load( $content, $wp_charset ) {
378 // Prevent removing closing tags in scripts.
379 $content_to_load = preg_replace( '/<script.*?<\/script>/si', '<!--\0-->', $content );
380
381 // check which priority the wpautop filter has; might have been disabled on purpose.
382 $wpautop_priority = has_filter( 'the_content', 'wpautop' );
383 if ( $wpautop_priority && Advanced_Ads_Plugin::get_instance()->get_content_injection_priority() < $wpautop_priority ) {
384 $content_to_load = wpautop( $content_to_load );
385 }
386
387 return $content_to_load;
388 }
389
390 /**
391 * Filter ad content.
392 *
393 * @param string $ad_content Ad content.
394 * @param string $tag_name tar before/after the content.
395 * @param array $options Injection options.
396 *
397 * @return string ad content.
398 */
399 private static function filter_ad_content( $ad_content, $tag_name, $options ) {
400 // Replace `</` with `<\/` in ad content when placed within `document.write()` to prevent code from breaking.
401 $ad_content = preg_replace( '#(document.write.+)</(.*)#', '$1<\/$2', $ad_content );
402
403 // Inject placeholder.
404 $id = count( self::$ads_for_placeholders );
405 self::$ads_for_placeholders[] = [
406 'id' => $id,
407 'tag' => $tag_name,
408 'position' => $options['position'],
409 'ad' => $ad_content,
410 ];
411
412 return '%advads_placeholder_' . $id . '%';
413 }
414
415 /**
416 * Prepare output.
417 *
418 * @param string $content Modified content.
419 * @param string $content_orig Original content.
420 *
421 * @return string $content Content to output.
422 */
423 private static function prepare_output( $content, $content_orig ) {
424 $content = self::inject_ads( $content, $content_orig, self::$ads_for_placeholders );
425 self::$ads_for_placeholders = [];
426
427 return $content;
428 }
429
430 /**
431 * Search for ad placeholders in the `$content` to determine positions at which to inject ads.
432 * Given the positions, inject ads into `$content_orig.
433 *
434 * @param string $content Post content with injected ad placeholders.
435 * @param string $content_orig Unmodified post content.
436 * @param array $ads_for_placeholders Array of ads.
437 * Each ad contains placeholder id, before or after which tag to inject the ad, the ad content.
438 *
439 * @return string $content
440 */
441 private static function inject_ads( $content, $content_orig, $ads_for_placeholders ) {
442 $self_closing_tags = [
443 'area',
444 'base',
445 'basefont',
446 'bgsound',
447 'br',
448 'col',
449 'embed',
450 'frame',
451 'hr',
452 'img',
453 'input',
454 'keygen',
455 'link',
456 'meta',
457 'param',
458 'source',
459 'track',
460 'wbr',
461 ];
462
463 // It is not possible to append/prepend in self closing tags.
464 foreach ( $ads_for_placeholders as &$ad_content ) {
465 if ( ( 'prepend' === $ad_content['position'] || 'append' === $ad_content['position'] )
466 && in_array( $ad_content['tag'], $self_closing_tags, true ) ) {
467 $ad_content['position'] = 'after';
468 }
469 }
470 unset( $ad_content );
471 usort( $ads_for_placeholders, [ 'Advanced_Ads_In_Content_Injector', 'sort_ads_for_placehoders' ] );
472
473 // Add tags before/after which ad placehoders were injected.
474 foreach ( $ads_for_placeholders as $ad_content ) {
475 $tag = $ad_content['tag'];
476
477 switch ( $ad_content['position'] ) {
478 case 'before':
479 case 'prepend':
480 $alts[] = "<{$tag}[^>]*>";
481 break;
482 case 'after':
483 if ( in_array( $tag, $self_closing_tags, true ) ) {
484 $alts[] = "<{$tag}[^>]*>";
485 } else {
486 $alts[] = "</{$tag}>";
487 }
488 break;
489 case 'append':
490 $alts[] = "</{$tag}>";
491 break;
492 }
493 }
494 $alts = array_unique( $alts );
495 $tag_regexp = implode( '|', $alts );
496 // Add ad placeholder.
497 $alts[] = '%advads_placeholder_(?:\d+)%';
498 $tag_and_placeholder_regexp = implode( '|', $alts );
499
500 preg_match_all( "#{$tag_and_placeholder_regexp}#i", $content, $tag_matches );
501 $count = 0;
502
503 // For each tag located before/after an ad placeholder, find its offset among the same tags.
504 foreach ( $tag_matches[0] as $r ) {
505 if ( preg_match( '/%advads_placeholder_(\d+)%/', $r, $result ) ) {
506 $id = $result[1];
507 $found_ad = false;
508 foreach ( $ads_for_placeholders as $n => $ad ) {
509 if ( (int) $ad['id'] === (int) $id ) {
510 $found_ad = $ad;
511 break;
512 }
513 }
514 if ( ! $found_ad ) {
515 continue;
516 }
517
518 switch ( $found_ad['position'] ) {
519 case 'before':
520 case 'append':
521 $ads_for_placeholders[ $n ]['offset'] = $count;
522 break;
523 case 'after':
524 case 'prepend':
525 $ads_for_placeholders[ $n ]['offset'] = $count - 1;
526 break;
527 }
528 } else {
529 $count ++;
530 }
531 }
532
533 // Find tags before/after which we need to inject ads.
534 preg_match_all( "#{$tag_regexp}#i", $content_orig, $orig_tag_matches, PREG_OFFSET_CAPTURE );
535 $new_content = '';
536 $pos = 0;
537
538 foreach ( $orig_tag_matches[0] as $n => $r ) {
539 $to_inject = [];
540 // Check if we need to inject an ad at this offset.
541 foreach ( $ads_for_placeholders as $ad ) {
542 if ( isset( $ad['offset'] ) && $ad['offset'] === $n ) {
543 $to_inject[] = $ad;
544 }
545 }
546
547 foreach ( $to_inject as $item ) {
548 switch ( $item['position'] ) {
549 case 'before':
550 case 'append':
551 $found_pos = $r[1];
552 break;
553 case 'after':
554 case 'prepend':
555 $found_pos = $r[1] + strlen( $r[0] );
556 break;
557 }
558
559 $new_content .= substr( $content_orig, $pos, $found_pos - $pos );
560 $pos = $found_pos;
561 $new_content .= $item['ad'];
562 }
563 }
564 $new_content .= substr( $content_orig, $pos );
565
566 return $new_content;
567 }
568
569
570 /**
571 * Callback function for usort() to sort ads for placeholders.
572 *
573 * @param array $first The first array to compare.
574 * @param array $second The second array to compare.
575 *
576 * @return int 0 if both objects equal. -1 if second array should come first, 1 otherwise.
577 */
578 public static function sort_ads_for_placehoders( $first, $second ) {
579 if ( $first['position'] === $second['position'] ) {
580 return 0;
581 }
582
583 $num = [
584 'before' => 1,
585 'prepend' => 2,
586 'append' => 3,
587 'after' => 4,
588 ];
589
590 return $num[ $first['position'] ] > $num[ $second['position'] ] ? 1 : - 1;
591 }
592
593 /**
594 * Add a warning to 'Ad health'.
595 *
596 * @param array $nodes .
597 *
598 * @return array $nodes.
599 */
600 public static function add_ad_health_node( $nodes ) {
601 $nodes[] = [
602 'type' => 1,
603 'data' => [
604 'parent' => 'advanced_ads_ad_health',
605 'id' => 'advanced_ads_ad_health_the_content_not_enough_elements',
606 'title' => sprintf(
607 /* translators: %s stands for the name of the "Disable level limitation" option and automatically translated as well */
608 __( 'Set <em>%s</em> to show more ads', 'advanced-ads' ),
609 __( 'Disable level limitation', 'advanced-ads' )
610 ),
611 'href' => admin_url( '/admin.php?page=advanced-ads-settings#top#general' ),
612 'meta' => [
613 'class' => 'advanced_ads_ad_health_warning',
614 'target' => '_blank',
615 ],
616 ],
617 ];
618
619 return $nodes;
620 }
621
622 /**
623 * Get paths of ancestors that should not contain ads.
624 *
625 * @param object $xpath DOMXPath object.
626 *
627 * @return array Paths of ancestors.
628 */
629 private static function get_ancestors_to_limit( $xpath ) {
630 $query = self::get_ancestors_to_limit_query();
631 if ( ! $query ) {
632 return [];
633 }
634
635 $node_list = $xpath->query( $query );
636 $ancestors_to_limit = [];
637
638 foreach ( $node_list as $a ) {
639 $ancestors_to_limit[] = $a->getNodePath();
640 }
641
642 return $ancestors_to_limit;
643 }
644
645
646 /**
647 * Remove paragraphs that has ancestors that should not contain ads.
648 *
649 * @param array $paragraphs An array of `DOMNode` objects to insert ads before or after.
650 * @param array $ancestors_to_limit Paths of ancestor that should not contain ads.
651 *
652 * @return array $new_paragraphs An array of `DOMNode` objects to insert ads before or after.
653 */
654 private static function filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit ) {
655 $new_paragraphs = [];
656
657 foreach ( $paragraphs as $k => $paragraph ) {
658 foreach ( $ancestors_to_limit as $a ) {
659 if ( 0 === stripos( $paragraph->getNodePath(), $a ) ) {
660 continue 2;
661 }
662 }
663
664 $new_paragraphs[] = $paragraph;
665 }
666
667 return $new_paragraphs;
668 }
669
670 /**
671 * Get query to select ancestors that should not contain ads.
672 *
673 * @return string/false DOMXPath query or false.
674 */
675 private static function get_ancestors_to_limit_query() {
676 /**
677 * TODO:
678 * - support `%` (rand) at the start
679 * - support plain text that node should contain instead of CSS selectors
680 * - support `prev` and `next` as `type`
681 */
682
683 /**
684 * Filter the nodes that limit injection.
685 *
686 * @param array An array of arrays, each of which contains:
687 *
688 * @type string $type Accept: `ancestor` - limit injection inside the ancestor.
689 * @type string $node A "class selector" which targets one class (.) or "id selector" which targets one id (#),
690 * optionally with `%` at the end.
691 */
692 $items = apply_filters(
693 'advanced-ads-content-injection-nodes-without-ads',
694 [
695 [
696 // a class anyone can use to prevent automatic ad injection into a specific element.
697 'node' => '.advads-stop-injection',
698 'type' => 'ancestor',
699 ],
700 [
701 // Product Slider for Beaver Builder by WooPack.
702 'node' => '.woopack-product-carousel',
703 'type' => 'ancestor',
704 ],
705 [
706 // WP Author Box Lite.
707 'node' => '#wpautbox-%',
708 'type' => 'ancestor',
709 ],
710 [
711 // GeoDirectory Post Slider.
712 'node' => '.geodir-post-slider',
713 'type' => 'ancestor',
714 ],
715 ]
716 );
717
718 $query = [];
719 foreach ( $items as $p ) {
720 $sel = $p['node'];
721
722 $sel_type = substr( $sel, 0, 1 );
723 $sel = substr( $sel, 1 );
724
725 $rand_pos = strpos( $sel, '%' );
726 $sel = str_replace( '%', '', $sel );
727 $sel = sanitize_html_class( $sel );
728
729 if ( '.' === $sel_type ) {
730 if ( false !== $rand_pos ) {
731 $query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel')";
732 } else {
733 $query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel ')";
734 }
735 }
736 if ( '#' === $sel_type ) {
737 if ( false !== $rand_pos ) {
738 $query[] = "@id and starts-with(@id, '$sel')";
739 } else {
740 $query[] = "@id and @id = '$sel'";
741 }
742 }
743 }
744
745 if ( ! $query ) {
746 return false;
747 }
748
749 return '//*[' . implode( ' or ', $query ) . ']';
750 }
751
752 }
753