PluginProbe ʕ •ᴥ•ʔ
Advanced Ads – Ad Manager & AdSense / 1.17.3
Advanced Ads – Ad Manager & AdSense v1.17.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 / ad_placements.php
advanced-ads / classes Last commit date
EDD_SL_Plugin_Updater.php 6 years ago ad-ajax.php 6 years ago ad-debug.php 8 years ago ad-health-notices.php 6 years ago ad-model.php 6 years ago ad-select.php 9 years ago ad.php 6 years ago ad_ajax_callbacks.php 6 years ago ad_group.php 6 years ago ad_placements.php 6 years ago ad_type_abstract.php 6 years ago ad_type_content.php 6 years ago ad_type_dummy.php 6 years ago ad_type_group.php 6 years ago ad_type_image.php 6 years ago ad_type_plain.php 6 years ago checks.php 6 years ago compatibility.php 6 years ago display-conditions.php 6 years ago filesystem.php 8 years ago frontend-notices.php 6 years ago frontend_checks.php 6 years ago plugin.php 6 years ago upgrades.php 7 years ago utils.php 6 years ago visitor-conditions.php 6 years ago widget.php 6 years ago
ad_placements.php
1044 lines
1 <?php
2
3 /**
4 * Advanced Ads
5 *
6 * @package Advanced_Ads_Placements
7 * @author Thomas Maier <support@wpadvancedads.com>
8 * @license GPL-2.0+
9 * @link https://wpadvancedads.com
10 * @copyright 2014 Thomas Maier, Advanced Ads GmbH
11 */
12
13 /**
14 * Grouping placements functions
15 *
16 * @since 1.1.0
17 * @package Advanced_Ads_Placements
18 * @author Thomas Maier <support@wpadvancedads.com>
19 */
20 class Advanced_Ads_Placements {
21
22 /**
23 * Gather placeholders which later are replaced by the ads
24 *
25 * @var array $ads_for_placeholders
26 */
27 private static $ads_for_placeholders = array();
28 /**
29 * Temporarily change content during processing
30 *
31 * @var array $placements
32 */
33 private static $replacements = array(
34 'gcse:search' => 'gcse__search', // Google custom search namespaced tags.
35 );
36
37 /**
38 * Get placement types
39 *
40 * @return array $types array with placement types
41 * @since 1.2.1
42 */
43 public static function get_placement_types() {
44 $types = array(
45 'default' => array(
46 'title' => __( 'Manual Placement', 'advanced-ads' ),
47 'description' => __( 'Manual placement to use as function or shortcode.', 'advanced-ads' ),
48 'image' => ADVADS_BASE_URL . 'admin/assets/img/placements/manual.png',
49 'options' => array(
50 'show_position' => true,
51 'show_lazy_load' => true,
52 'amp' => true,
53 ),
54 ),
55 'header' => array(
56 'title' => __( 'Header Code', 'advanced-ads' ),
57 'description' => __( 'Injected in Header (before closing &lt;/head&gt; Tag, often not visible).', 'advanced-ads' ),
58 'image' => ADVADS_BASE_URL . 'admin/assets/img/placements/header.png',
59 ),
60 'footer' => array(
61 'title' => __( 'Footer Code', 'advanced-ads' ),
62 'description' => __( 'Injected in Footer (before closing &lt;/body&gt; Tag).', 'advanced-ads' ),
63 'image' => ADVADS_BASE_URL . 'admin/assets/img/placements/footer.png',
64 'options' => array( 'amp' => true ),
65 ),
66 'post_top' => array(
67 'title' => __( 'Before Content', 'advanced-ads' ),
68 'description' => __( 'Injected before the post content.', 'advanced-ads' ),
69 'image' => ADVADS_BASE_URL . 'admin/assets/img/placements/content-before.png',
70 'options' => array(
71 'show_position' => true,
72 'show_lazy_load' => true,
73 'uses_the_content' => true,
74 'amp' => true,
75 ),
76 ),
77 'post_bottom' => array(
78 'title' => __( 'After Content', 'advanced-ads' ),
79 'description' => __( 'Injected after the post content.', 'advanced-ads' ),
80 'image' => ADVADS_BASE_URL . 'admin/assets/img/placements/content-after.png',
81 'options' => array(
82 'show_position' => true,
83 'show_lazy_load' => true,
84 'uses_the_content' => true,
85 'amp' => true,
86 ),
87 ),
88 'post_content' => array(
89 'title' => __( 'Content', 'advanced-ads' ),
90 'description' => __( 'Injected into the content. You can choose the paragraph after which the ad content is displayed.', 'advanced-ads' ),
91 'image' => ADVADS_BASE_URL . 'admin/assets/img/placements/content-within.png',
92 'options' => array(
93 'show_position' => true,
94 'show_lazy_load' => true,
95 'uses_the_content' => true,
96 'amp' => true,
97 ),
98 ),
99 'sidebar_widget' => array(
100 'title' => __( 'Sidebar Widget', 'advanced-ads' ),
101 'description' => __( 'Create a sidebar widget with an ad. Can be placed and used like any other widget.', 'advanced-ads' ),
102 'image' => ADVADS_BASE_URL . 'admin/assets/img/placements/widget.png',
103 'options' => array(
104 'show_lazy_load' => true,
105 'amp' => true,
106 ),
107 ),
108 );
109
110 return apply_filters( 'advanced-ads-placement-types', $types );
111 }
112
113 /**
114 * Update placements if sent
115 *
116 * @since 1.5.2
117 */
118 public static function update_placements() {
119
120 // check user permissions.
121 if ( ! current_user_can( Advanced_Ads_Plugin::user_cap( 'advanced_ads_manage_placements' ) ) ) {
122 return;
123 }
124
125 $success = null;
126
127 // add hook of last opened placement settings to URL.
128 $hook = ! empty( $_POST['advads-last-edited-placement'] ) ? '#single-placement-' . $_POST['advads-last-edited-placement'] : '';
129
130 if ( isset( $_POST['advads']['placement'] ) && check_admin_referer( 'advads-placement', 'advads_placement' ) ) {
131 $success = self::save_new_placement( $_POST['advads']['placement'] );
132 }
133 // save placement data.
134 if ( isset( $_POST['advads']['placements'] ) && check_admin_referer( 'advads-placement', 'advads_placement' ) ) {
135 $success = self::save_placements( $_POST['advads']['placements'] );
136 }
137
138 $success = apply_filters( 'advanced-ads-update-placements', $success );
139
140 if ( isset( $success ) ) {
141 $message = $success ? 'updated' : 'error';
142 wp_redirect( esc_url_raw( add_query_arg( array( 'message' => $message ) ) ) . $hook );
143 }
144 }
145
146 /**
147 * Save a new placement
148 *
149 * @param array $new_placement information about the new placement.
150 *
151 * @return mixed slug if saved; false if not
152 * @since 1.1.0
153 */
154 public static function save_new_placement( $new_placement ) {
155 // load placements // -TODO use model.
156 $placements = Advanced_Ads::get_ad_placements_array();
157
158 // create slug.
159 $new_placement['slug'] = sanitize_title( $new_placement['name'] );
160
161 if ( isset( $placements[ $new_placement['slug'] ] ) ) {
162 $i = 1;
163 // try to save placement until we found an empty slug.
164 do {
165 $i ++;
166 if ( 100 === $i ) { // prevent endless loop, just in case.
167 Advanced_Ads::log( 'endless loop when injecting placement' );
168 break;
169 }
170 } while ( isset( $placements[ $new_placement['slug'] . '_' . $i ] ) );
171
172 $new_placement['slug'] .= '_' . $i;
173 $new_placement['name'] .= ' ' . $i;
174 }
175
176 // check if slug already exists or is empty.
177 if ( '' === $new_placement['slug'] || isset( $placements[ $new_placement['slug'] ] ) || ! isset( $new_placement['type'] ) ) {
178 return false;
179 }
180
181 // make sure only allowed types are being saved.
182 $placement_types = self::get_placement_types();
183 $new_placement['type'] = ( isset( $placement_types[ $new_placement['type'] ] ) ) ? $new_placement['type'] : 'default';
184 // escape name.
185 $new_placement['name'] = esc_attr( $new_placement['name'] );
186
187 // add new place to all placements.
188 $placements[ $new_placement['slug'] ] = array(
189 'type' => $new_placement['type'],
190 'name' => $new_placement['name'],
191 'item' => $new_placement['item'],
192 );
193
194 // add index options.
195 if ( isset( $new_placement['options'] ) ) {
196 $placements[ $new_placement['slug'] ]['options'] = $new_placement['options'];
197 if ( isset( $placements[ $new_placement['slug'] ]['options']['index'] ) ) {
198 $placements[ $new_placement['slug'] ]['options']['index'] = absint( $placements[ $new_placement['slug'] ]['options']['index'] );
199 }
200 }
201
202 // save array.
203 Advanced_Ads::get_instance()->get_model()->update_ad_placements_array( $placements );
204
205 return $new_placement['slug'];
206 }
207
208 /**
209 * Save placements
210 *
211 * @param array $placement_items placements.
212 *
213 * @return mixed true if saved; error message if not
214 * @since 1.1.0
215 */
216 public static function save_placements( $placement_items ) {
217
218 // load placements // -TODO use model.
219 $placements = Advanced_Ads::get_ad_placements_array();
220
221 foreach ( $placement_items as $_placement_slug => $_placement ) {
222 // remove the placement.
223 if ( isset( $_placement['delete'] ) ) {
224 unset( $placements[ $_placement_slug ] );
225 continue;
226 }
227 // save item.
228 if ( isset( $_placement['item'] ) ) {
229 $placements[ $_placement_slug ]['item'] = $_placement['item'];
230 }
231 // save item options.
232 if ( isset( $_placement['options'] ) ) {
233 $placements[ $_placement_slug ]['options'] = $_placement['options'];
234 if ( isset( $placements[ $_placement_slug ]['options']['index'] ) ) {
235 $placements[ $_placement_slug ]['options']['index'] = absint( $placements[ $_placement_slug ]['options']['index'] );
236 }
237 } else {
238 $placements[ $_placement_slug ]['options'] = array();
239 }
240 }
241
242 // save array.
243 Advanced_Ads::get_instance()->get_model()->update_ad_placements_array( $placements );
244
245 return true;
246 }
247
248 /**
249 * Get items for item select field
250 *
251 * @return array $select items for select field
252 * @since 1.1
253 */
254 public static function items_for_select() {
255 $select = array();
256 $model = Advanced_Ads::get_instance()->get_model();
257
258 // load all ad groups.
259 $groups = $model->get_ad_groups();
260 foreach ( $groups as $_group ) {
261 $select['groups'][ 'group_' . $_group->term_id ] = $_group->name;
262 }
263
264 // load all ads.
265 $ads = $model->get_ads(
266 array(
267 'orderby' => 'title',
268 'order' => 'ASC',
269 )
270 );
271 foreach ( $ads as $_ad ) {
272 $select['ads'][ 'ad_' . $_ad->ID ] = $_ad->post_title;
273 }
274
275 return $select;
276 }
277
278 /**
279 * Get html tags for content injection
280 *
281 * @return array $tags array with tags that can be used for content injection
282 * @since 1.3.5
283 */
284 public static function tags_for_content_injection() {
285 $tags = apply_filters(
286 'advanced-ads-tags-for-injection',
287 array(
288 // translators: %s is an html tag.
289 'p' => sprintf( __( 'paragraph (%s)', 'advanced-ads' ), '&lt;p&gt;' ),
290 // translators: %s is an html tag.
291 'pwithoutimg' => sprintf( __( 'paragraph without image (%s)', 'advanced-ads' ), '&lt;p&gt;' ),
292 // translators: %s is an html tag.
293 'h2' => sprintf( __( 'headline 2 (%s)', 'advanced-ads' ), '&lt;h2&gt;' ),
294 // translators: %s is an html tag.
295 'h3' => sprintf( __( 'headline 3 (%s)', 'advanced-ads' ), '&lt;h3&gt;' ),
296 // translators: %s is an html tag.
297 'h4' => sprintf( __( 'headline 4 (%s)', 'advanced-ads' ), '&lt;h4&gt;' ),
298 )
299 );
300
301 return $tags;
302 }
303
304 /**
305 * Return content of a placement
306 *
307 * @param string $id slug of the display.
308 * @param array $args optional arguments (passed to child).
309 *
310 * @return string
311 */
312 public static function output( $id = '', $args = array() ) {
313 // get placement data for the slug.
314 if ( '' == $id ) {
315 return;
316 }
317
318 $placements = Advanced_Ads::get_ad_placements_array();
319 $placement = ( isset( $placements[ $id ] ) && is_array( $placements[ $id ] ) ) ? $placements[ $id ] : array();
320
321 if ( isset( $args['change-placement'] ) ) {
322 // some options was provided by the user.
323 $placement = Advanced_Ads_Utils::merge_deep_array( array( $placement, $args['change-placement'] ) );
324 }
325
326 if ( isset( $placement['item'] ) && '' !== $placement['item'] ) {
327 $_item = explode( '_', $placement['item'] );
328
329 if ( ! isset( $_item[1] ) || empty( $_item[1] ) ) {
330 return;
331 }
332
333 // inject options.
334 if ( isset( $placement['options'] ) && is_array( $placement['options'] ) ) {
335 foreach ( $placement['options'] as $_k => $_v ) {
336 if ( ! isset( $args[ $_k ] ) ) {
337 $args[ $_k ] = $_v;
338 }
339 }
340 }
341
342 // inject placement type.
343 if ( isset( $placement['type'] ) ) {
344 $args['placement_type'] = $placement['type'];
345 }
346
347 // options.
348 $prefix = Advanced_Ads_Plugin::get_instance()->get_frontend_prefix();
349
350 // return either ad or group content.
351 switch ( $_item[0] ) {
352 case 'ad':
353 case Advanced_Ads_Select::AD:
354 // create class from placement id (not if header injection).
355 if ( ! isset( $placement['type'] ) || 'header' !== $placement['type'] ) {
356 if ( ! isset( $args['output'] ) ) {
357 $args['output'] = array();
358 }
359 if ( ! isset( $args['output']['class'] ) ) {
360 $args['output']['class'] = array();
361 }
362 $class = $prefix . $id;
363 if ( ! in_array( $class, $args['output']['class'] ) ) {
364 $args['output']['class'][] = $class;
365 }
366 }
367
368 // fix method id.
369 $_item[0] = Advanced_Ads_Select::AD;
370 break;
371
372 // avoid loops (programmatical error).
373 case Advanced_Ads_Select::PLACEMENT:
374 return;
375
376 case Advanced_Ads_Select::GROUP:
377 $class = $prefix . $id;
378 if ( ( isset( $placement['type'] ) && $placement['type'] !== 'header' )
379 && ( ! isset( $args['output']['class'] )
380 || ! is_array( $args['output']['class'] )
381 || ! in_array( $class, $args['output']['class'] ) ) ) {
382 $args['output']['class'][] = $class;
383 }
384 default:
385 }
386
387 // create placement id for various features.
388 $args['output']['placement_id'] = $id;
389
390 // add the placement to the global output array.
391 $advads = Advanced_Ads::get_instance();
392 $name = isset( $placement['name'] ) ? $placement['name'] : $id;
393
394 if ( ! isset( $args['global_output'] ) || $args['global_output'] ) {
395 $advads->current_ads[] = array(
396 'type' => 'placement',
397 'id' => $id,
398 'title' => $name,
399 );
400 }
401
402 $result = Advanced_Ads_Select::get_instance()->get_ad_by_method( (int) $_item[1], $_item[0], $args );
403
404 return $result;
405 }
406
407 return;
408 }
409
410 /**
411 * Inject ads directly into the content
412 *
413 * @param string $placement_id Id of the placement.
414 * @param array $placement_opts Placement options.
415 * @param string $content Content to inject placement into.
416 *
417 * @return string $content Content with injected placement.
418 * @since 1.2.1
419 */
420 public static function &inject_in_content( $placement_id, $placement_opts, &$content ) {
421 if ( ! extension_loaded( 'dom' ) ) {
422 return $content;
423 }
424
425 // get plugin options.
426 $plugin_options = Advanced_Ads::get_instance()->options();
427
428 $wp_charset = get_bloginfo( 'charset' );
429 // parse document as DOM (fragment - having only a part of an actual post given).
430
431 $content_to_load = self::get_content_to_load( $content, $wp_charset );
432 if ( ! $content_to_load ) {
433 return $content;
434 }
435
436 $dom = new DOMDocument( '1.0', $wp_charset );
437 // may loose some fragments or add autop-like code.
438 libxml_use_internal_errors( true ); // avoid notices and warnings - html is most likely malformed.
439
440 $success = $dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $content_to_load );
441 libxml_use_internal_errors( false );
442 if ( true !== $success ) {
443 // -TODO handle cases were dom-parsing failed (at least inform user)
444 return $content;
445 }
446
447 // parse arguments.
448 $tag = isset( $placement_opts['tag'] ) ? $placement_opts['tag'] : 'p';
449 $tag = preg_replace( '/[^a-z0-9]/i', '', $tag ); // simplify tag.
450
451 // allow more complex xPath expression.
452 $tag = apply_filters( 'advanced-ads-placement-content-injection-xpath', $tag, $placement_opts );
453
454 if ( 'pwithoutimg' === $tag ) {
455 $tag = 'p[not(descendant::img)]';
456 }
457
458 // select positions.
459 $xpath = new DOMXPath( $dom );
460 $items = $xpath->query( '/html/body/' . $tag );
461
462 $options = array(
463 'allowEmpty' => false, // whether the tag can be empty to be counted.
464 'paragraph_select_from_bottom' => isset( $placement_opts['start_from_bottom'] ) && $placement_opts['start_from_bottom'],
465 // only has before and after.
466 'before' => isset( $placement_opts['position'] ) && 'before' === $placement_opts['position'],
467 );
468
469 $options['paragraph_id'] = isset( $placement_opts['index'] ) ? $placement_opts['index'] : 1;
470 $options['paragraph_id'] = max( 1, (int) $options['paragraph_id'] );
471
472 // if there are too few items at this level test nesting.
473 $options['itemLimit'] = 'p' === $tag ? 2 : 1;
474
475 // trigger such a high item limit that all elements will be considered.
476 if ( ! empty( $plugin_options['content-injection-level-disabled'] ) ) {
477 $options['itemLimit'] = 1000;
478 }
479
480 // allow hooks to change some options.
481 $options = apply_filters(
482 'advanced-ads-placement-content-injection-options',
483 $options,
484 $tag
485 );
486
487 if ( $items->length < $options['itemLimit'] ) {
488 $items = $xpath->query( '/html/body/*/' . $tag );
489 }
490 // try third level.
491 if ( $items->length < $options['itemLimit'] ) {
492 $items = $xpath->query( '/html/body/*/*/' . $tag );
493 }
494 // try all levels as last resort.
495 if ( $items->length < $options['itemLimit'] ) {
496 $items = $xpath->query( '//' . $tag );
497 }
498
499 // allow to select other elements.
500 $items = apply_filters( 'advanced-ads-placement-content-injection-items', $items, $xpath, $tag );
501
502 // filter empty tags from items.
503 $whitespaces = json_decode( '"\t\n\r \u00A0"' );
504 $paragraphs = array();
505 foreach ( $items as $item ) {
506 if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
507 $paragraphs[] = $item;
508 }
509 }
510
511 $ancestors_to_limit = self::get_ancestors_to_limit( $xpath );
512 $paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
513
514 $options['paragraph_count'] = count( $paragraphs );
515
516 if ( $options['paragraph_count'] >= $options['paragraph_id'] ) {
517 $offset = $options['paragraph_select_from_bottom'] ? $options['paragraph_count'] - $options['paragraph_id'] : $options['paragraph_id'] - 1;
518 $offsets = apply_filters( 'advanced-ads-placement-content-offsets', array( $offset ), $options, $placement_opts );
519 $did_inject = false;
520
521 foreach ( $offsets as $offset ) {
522 // inject.
523 $node = apply_filters( 'advanced-ads-placement-content-injection-node', $paragraphs[ $offset ], $tag, $options['before'] );
524
525 $ad_content = Advanced_Ads_Select::get_instance()->get_ad_by_method( $placement_id, 'placement', $placement_opts );
526
527 if ( trim( $ad_content, $whitespaces ) === '' ) {
528 continue;
529 }
530
531 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
532 $ad_content = self::filter_ad_content( $ad_content, $node->tagName, $options );
533
534 // convert HTML to XML!
535 $ad_dom = new DOMDocument( '1.0', $wp_charset );
536 libxml_use_internal_errors( true );
537 $ad_dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $ad_content );
538 // log errors.
539 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && current_user_can( 'advanced_ads_manage_options' ) ) {
540 foreach ( libxml_get_errors() as $_error ) {
541 // continue, if there is '&' symbol, but not HTML entity.
542 if ( false === stripos( $_error->message, 'htmlParseEntityRef:' ) ) {
543 Advanced_Ads::log( 'possible content injection error for placement "' . $placement_id . '": ' . print_r( $_error, true ) );
544 }
545 }
546 }
547
548 if ( $options['before'] ) {
549 $ref_node = $node;
550
551 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
552 $importedNode = $dom->importNode( $importedNode, true );
553 $ref_node->parentNode->insertBefore( $importedNode, $ref_node );
554 }
555 } else {
556 // append before next node or as last child to body.
557 $ref_node = $node->nextSibling;
558 if ( isset( $ref_node ) ) {
559
560 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
561 $importedNode = $dom->importNode( $importedNode, true );
562 $ref_node->parentNode->insertBefore( $importedNode, $ref_node );
563 }
564 } else {
565 // append to body; -TODO using here that we only select direct children of the body tag.
566 foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
567 $importedNode = $dom->importNode( $importedNode, true );
568 $node->parentNode->appendChild( $importedNode );
569 }
570 }
571 }
572
573 libxml_use_internal_errors( false );
574 $did_inject = true;
575 }
576
577 if ( ! $did_inject ) {
578 return $content;
579 }
580
581 $content_orig = $content;
582 // convert to text-representation.
583 $content = $dom->saveHTML();
584 $content = self::prepare_output( $content, $content_orig );
585
586 /**
587 * Show a warning to ad admins in the Ad Health bar in the frontend, when
588 *
589 * * the level limitation was not disabled
590 * * could not inject one ad (as by use of `elseif` here)
591 * * but there are enough elements on the site, but just in sub-containers
592 */
593 } elseif ( current_user_can( Advanced_Ads_Plugin::user_cap( 'advanced_ads_manage_options' ) )
594 && empty( $plugin_options['content-injection-level-disabled'] ) ) {
595
596 // Check if there are more elements without limitation.
597 $all_items = $xpath->query( '//' . $tag );
598
599 $paragraphs = array();
600 foreach ( $all_items as $item ) {
601 if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
602 $paragraphs[] = $item;
603 }
604 }
605
606 $paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
607 if ( $options['paragraph_id'] <= count( $paragraphs ) ) {
608 // Add a warning to ad health.
609 add_filter( 'advanced-ads-ad-health-nodes', array( 'Advanced_Ads_Placements', 'add_ad_health_node' ) );
610 }
611 }
612
613 return $content;
614 }
615
616 /**
617 * Get content to load.
618 *
619 * @param string $content Original content.
620 * @param string $wp_charset blog charset.
621 *
622 * @return string $content Content to load.
623 */
624 private static function get_content_to_load( $content, $wp_charset ) {
625 $plugin_options = Advanced_Ads::get_instance()->options();
626
627 // Prevent removing closing tags in scripts.
628 $content_to_load = preg_replace( '/<script.*?<\/script>/', '<!--\0-->', $content );
629
630 // check which priority the wpautop filter has; might have been disabled on purpose.
631 $wpautop_priority = has_filter( 'the_content', 'wpautop' );
632 if ( $wpautop_priority && Advanced_Ads_Plugin::get_instance()->get_content_injection_priority() < $wpautop_priority ) {
633 $content_to_load = wpautop( $content_to_load );
634 }
635
636 return $content_to_load;
637 }
638
639 /**
640 * Filter ad content.
641 *
642 * @param string $ad_content Ad content.
643 * @param string $tag_name tar before/after the content.
644 * @param array $options Injection options.
645 *
646 * @return string ad content.
647 */
648 private static function filter_ad_content( $ad_content, $tag_name, $options ) {
649 $plugin_options = Advanced_Ads::get_instance()->options();
650
651 // Inject placeholder.
652 $id = count( self::$ads_for_placeholders );
653 self::$ads_for_placeholders[] = array(
654 'id' => $id,
655 'tag' => $tag_name,
656 'type' => $options['before'] ? 'before' : 'after',
657 'ad' => $ad_content,
658 );
659 $ad_content = '%advads_placeholder_' . $id . '%';
660
661 return $ad_content;
662 }
663
664 /**
665 * Prepare output.
666 *
667 * @param string $content Modified content.
668 * @param string $content_orig Original content.
669 *
670 * @return string $content Content to output.
671 */
672 private static function prepare_output( $content, $content_orig ) {
673 $plugin_options = Advanced_Ads::get_instance()->options();
674
675 $content = self::inject_ads( $content, $content_orig, self::$ads_for_placeholders );
676 self::$ads_for_placeholders = array();
677
678 return $content;
679 }
680
681 /**
682 * Search for ad placeholders in the `$content` to determine positions at which to inject ads.
683 * Given the positions, inject ads into `$content_orig.
684 *
685 * @param string $content Post content with injected ad placeholders.
686 * @param string $content_orig Unmodified post content.
687 * @param array $options Injection options.
688 * @param array $ads_for_placeholders Array of ads.
689 * Each ad contains placeholder id, before or after which tag to inject the ad, the ad content.
690 *
691 * @return string $content
692 */
693 private static function inject_ads( $content, $content_orig, $ads_for_placeholders ) {
694 $self_closing_tags = array(
695 'area',
696 'base',
697 'basefont',
698 'bgsound',
699 'br',
700 'col',
701 'embed',
702 'frame',
703 'hr',
704 'img',
705 'input',
706 'keygen',
707 'link',
708 'meta',
709 'param',
710 'source',
711 'track',
712 'wbr',
713 );
714
715 // It is not possible to append/prepend in self closing tags.
716 foreach ( $ads_for_placeholders as &$ad_content ) {
717 if ( ( 'prepend' === $ad_content['type'] || 'append' === $ad_content['type'] )
718 && in_array( $ad_content['tag'], $self_closing_tags, true ) ) {
719 $ad_content['type'] = 'after';
720 }
721 }
722 unset( $ad_content );
723 usort( $ads_for_placeholders, array( 'Advanced_Ads_Placements', 'sort_ads_for_placehoders' ) );
724
725 // Add tags before/after which ad placehoders were injected.
726 foreach ( $ads_for_placeholders as $ad_content ) {
727 $tag = $ad_content['tag'];
728
729 switch ( $ad_content['type'] ) {
730 case 'before':
731 case 'prepend':
732 $alts[] = "<${tag}[^>]*>";
733 break;
734 case 'after':
735 if ( in_array( $tag, $self_closing_tags, true ) ) {
736 $alts[] = "<${tag}[^>]*>";
737 } else {
738 $alts[] = "</${tag}>";
739 }
740 break;
741 case 'append':
742 $alts[] = "</${tag}>";
743 break;
744 }
745 }
746 $alts = array_unique( $alts );
747 $tag_regexp = implode( '|', $alts );
748 // Add ad placeholder.
749 $alts[] = '%advads_placeholder_(?:\d+)%';
750 $tag_and_placeholder_regexp = implode( '|', $alts );
751
752 preg_match_all( "#{$tag_and_placeholder_regexp}#i", $content, $tag_matches );
753 $count = 0;
754
755 // For each tag located before/after an ad placeholder, find its offset among the same tags.
756 foreach ( $tag_matches[0] as $r ) {
757 if ( preg_match( '/%advads_placeholder_(\d+)%/', $r, $result ) ) {
758 $id = $result[1];
759 $found_ad = false;
760 foreach ( $ads_for_placeholders as $n => $ad ) {
761 if ( (int) $ad['id'] === (int) $id ) {
762 $found_ad = $ad;
763 break;
764 }
765 }
766 if ( ! $found_ad ) {
767 continue;
768 }
769
770 switch ( $found_ad['type'] ) {
771 case 'before':
772 case 'append':
773 $ads_for_placeholders[ $n ]['offset'] = $count;
774 break;
775 case 'after':
776 case 'prepend':
777 $ads_for_placeholders[ $n ]['offset'] = $count - 1;
778 break;
779 }
780 } else {
781 $count ++;
782 }
783 }
784
785 // Find tags before/after which we need to inject ads.
786 preg_match_all( "#{$tag_regexp}#i", $content_orig, $orig_tag_matches, PREG_OFFSET_CAPTURE );
787 $new_content = '';
788 $pos = 0;
789
790 foreach ( $orig_tag_matches[0] as $n => $r ) {
791 $to_inject = array();
792 // Check if we need to inject an ad at this offset.
793 foreach ( $ads_for_placeholders as $ad ) {
794 if ( isset( $ad['offset'] ) && $ad['offset'] === $n ) {
795 $to_inject[] = $ad;
796 }
797 }
798
799 foreach ( $to_inject as $item ) {
800 switch ( $item['type'] ) {
801 case 'before':
802 case 'append':
803 $found_pos = $r[1];
804 break;
805 case 'after':
806 case 'prepend':
807 $found_pos = $r[1] + strlen( $r[0] );
808 break;
809 }
810
811 $new_content .= substr( $content_orig, $pos, $found_pos - $pos );
812 $pos = $found_pos;
813 $new_content .= $item['ad'];
814 }
815 }
816 $new_content .= substr( $content_orig, $pos );
817
818 return $new_content;
819 }
820
821
822 /**
823 * Callback function for usort() to sort ads for placeholders.
824 *
825 * @param array $first The first array to compare.
826 * @param array $second The second array to compare.
827 *
828 * @return int 0 if both objects equal. -1 if second array should come first, 1 otherwise.
829 */
830 public static function sort_ads_for_placehoders( $first, $second ) {
831 if ( $first['type'] === $second['type'] ) {
832 return 0;
833 }
834
835 $num = array(
836 'before' => 1,
837 'prepend' => 2,
838 'append' => 3,
839 'after' => 4,
840 );
841
842 return $num[ $first['type'] ] > $num[ $second['type'] ] ? 1 : - 1;
843 }
844
845 /**
846 * Add a warning to 'Ad health'.
847 *
848 * @param array $nodes .
849 *
850 * @return array $nodes.
851 */
852 public static function add_ad_health_node( $nodes ) {
853 $nodes[] = array(
854 'type' => 1,
855 'data' => array(
856 'parent' => 'advanced_ads_ad_health',
857 'id' => 'advanced_ads_ad_health_the_content_not_enough_elements',
858 'title' => sprintf(
859 /* translators: %s stands for the name of the "Disable level limitation" option and automatically translated as well */
860 __( 'Set <em>%s</em> to show more ads', 'advanced-ads' ),
861 __( 'Disable level limitation', 'advanced-ads' )
862 ),
863 'href' => admin_url( '/admin.php?page=advanced-ads-settings#top#general' ),
864 'meta' => array(
865 'class' => 'advanced_ads_ad_health_warning',
866 'target' => '_blank',
867 ),
868 ),
869 );
870
871 return $nodes;
872 }
873
874 /**
875 * Check if the placement can be displayed
876 *
877 * @param int $id placement id.
878 *
879 * @return bool true if placement can be displayed
880 * @since 1.6.9
881 */
882 public static function can_display( $id = 0 ) {
883 if ( ! isset( $id ) || 0 === $id ) {
884 return true;
885 }
886
887 return apply_filters( 'advanced-ads-can-display-placement', true, $id );
888 }
889
890 /**
891 * Get the placements that includes the ad or group.
892 *
893 * @param string $type 'ad' or 'group'.
894 * @param int $id Id.
895 *
896 * @return array
897 */
898 public static function get_placements_by( $type, $id ) {
899 $result = array();
900
901 $placements = Advanced_Ads::get_ad_placements_array();
902 foreach ( $placements as $_id => $_placement ) {
903 if ( isset( $_placement['item'] ) && $_placement['item'] === $type . '_' . $id ) {
904 $result[ $_id ] = $_placement;
905 }
906 }
907
908 return $result;
909 }
910
911 /**
912 * Get paths of ancestors that should not contain ads.
913 *
914 * @param object $xpath DOMXPath object.
915 *
916 * @return array Paths of ancestors.
917 */
918 private static function get_ancestors_to_limit( $xpath ) {
919 $query = self::get_ancestors_to_limit_query();
920 if ( ! $query ) {
921 return array();
922 }
923
924 $node_list = $xpath->query( $query );
925 $ancestors_to_limit = array();
926
927 foreach ( $node_list as $a ) {
928 $ancestors_to_limit[] = $a->getNodePath();
929 }
930
931 return $ancestors_to_limit;
932 }
933
934
935 /**
936 * Remove paragraphs that has ancestors that should not contain ads.
937 *
938 * @param array $paragraphs An array of `DOMNode` objects to insert ads before or after.
939 * @param array $ancestors_to_limit Paths of ancestor that should not contain ads.
940 *
941 * @return array $new_paragraphs An array of `DOMNode` objects to insert ads before or after.
942 */
943 private static function filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit ) {
944 $new_paragraphs = array();
945
946 foreach ( $paragraphs as $k => $paragraph ) {
947 foreach ( $ancestors_to_limit as $a ) {
948 if ( 0 === stripos( $paragraph->getNodePath(), $a ) ) {
949 continue 2;
950 }
951 }
952
953 $new_paragraphs[] = $paragraph;
954 }
955
956 return $new_paragraphs;
957 }
958
959 /**
960 * Get query to select ancestors that should not contain ads.
961 *
962 * @return string/false DOMXPath query or false.
963 */
964 private static function get_ancestors_to_limit_query() {
965 /**
966 * TODO:
967 * - support `%` (rand) at the start
968 * - support plain text that node should contain instead of CSS selectors
969 * - support `prev` and `next` as `type`
970 */
971
972 /**
973 * Filter the nodes that limit injection.
974 *
975 * @param array An array of arrays, each of which contains:
976 *
977 * @type string $type Accept: `ancestor` - limit injection inside the ancestor.
978 * @type string $node A "class selector" which targets one class (.) or "id selector" which targets one id (#),
979 * optionally with `%` at the end.
980 */
981 $items = apply_filters(
982 'advanced-ads-content-injection-nodes-without-ads',
983 array(
984 array(
985 // a class anyone can use to prevent automatic ad injection into a specific element.
986 'node' => '.advads-stop-injection',
987 'type' => 'ancestor',
988 ),
989 array(
990 // Product Slider for Beaver Builder by WooPack.
991 'node' => '.woopack-product-carousel',
992 'type' => 'ancestor',
993 ),
994 array(
995 // WP Author Box Lite.
996 'node' => '#wpautbox-%',
997 'type' => 'ancestor',
998 ),
999 array(
1000 // GeoDirectory Post Slider.
1001 'node' => '.geodir-post-slider',
1002 'type' => 'ancestor',
1003 ),
1004 )
1005 );
1006
1007 $query = array();
1008 foreach ( $items as $p ) {
1009 $sel = $p['node'];
1010
1011 $sel_type = substr( $sel, 0, 1 );
1012 $sel = substr( $sel, 1 );
1013
1014 $rand_pos = strpos( $sel, '%' );
1015 $sel = str_replace( '%', '', $sel );
1016 $sel = sanitize_html_class( $sel );
1017
1018 if ( '.' === $sel_type ) {
1019 if ( false !== $rand_pos ) {
1020 $query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel')";
1021 } else {
1022 $query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel ')";
1023 }
1024 }
1025 if ( '#' === $sel_type ) {
1026 if ( false !== $rand_pos ) {
1027 $query[] = "@id and starts-with(@id, '$sel')";
1028 } else {
1029 $query[] = "@id and @id = '$sel'";
1030 }
1031 }
1032 }
1033
1034 if ( ! $query ) {
1035 return false;
1036 }
1037
1038 return '//*[' . implode( ' or ', $query ) . ']';
1039 }
1040
1041
1042 }
1043
1044