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