* @since 1.48.0
*/
namespace AdvancedAds\Abstracts;
use Advanced_Ads;
use Advanced_Ads_Inline_Css;
use Advanced_Ads_Utils;
use Advanced_Ads_Visitor_Conditions;
use AdvancedAds\Traits;
use AdvancedAds\Constants;
use AdvancedAds\Frontend\Stats;
use AdvancedAds\Interfaces\Ad_Type;
use AdvancedAds\Utilities\WordPress;
use AdvancedAds\Utilities\Conditional;
use AdvancedAds\Framework\Utilities\Arr;
use AdvancedAds\Framework\Utilities\Str;
use AdvancedAds\Compatibility\Compatibility;
use AdvancedAds\Framework\Utilities\Formatting;
defined( 'ABSPATH' ) || exit;
/**
* Ad.
*/
abstract class Ad extends Data {
use Traits\Entity;
use Traits\Wrapper;
/**
* This is the object type.
*
* @var string
*/
protected $object_type = 'ad';
/**
* The label for the ad.
*
* @var string|null
*/
private $label = null;
/**
* Hold groups within the ad
*
* @var Group[]
*/
private $groups = null;
/**
* Wrapper for the ad.
*
* @var array|null
*/
protected $wrapper = null;
/**
* Core data for this object. Name value pairs (name + default value).
*
* @var array
*/
protected $data = [
'title' => '',
'slug' => '',
'status' => false,
'author_id' => 0,
'content' => '',
'description' => '',
'width' => 0,
'height' => 0,
'visitors' => [],
'conditions' => [],
'has_weekdays' => false,
'weekdays' => [],
'type' => 'dummy',
'url' => '',
'expiry_date' => false,
'allow_php' => false,
'allow_shortcodes' => false,
'reserve_space' => false,
'debugmode' => false,
'wrapper-id' => '',
'wrapper-class' => '',
'position' => 'none',
'clearfix' => false,
'margin' => [
'top' => 0,
'right' => 0,
'bottom' => 0,
'left' => 0,
],
];
/**
* Get the ad if ID is passed, otherwise the ad is new and empty.
*
* @param Ad|WP_Post|int $ad Ad to init.
*/
public function __construct( $ad = 0 ) {
parent::__construct();
$this->set_ad_id( $ad );
$this->data_store = wp_advads_get_ad_repository();
if ( $this->get_id() > 0 ) {
$this->data_store->read( $this );
}
}
/**
* Set the ad ID depending on what was passed.
*
* @param Ad|WP_Post|int $ad Ad instance, post instance or numeric.
*
* @return void
*/
private function set_ad_id( $ad ): void {
if ( is_numeric( $ad ) && $ad > 0 ) {
$this->set_id( $ad );
} elseif ( $ad instanceof self ) {
$this->set_id( absint( $ad->get_id() ) );
} elseif ( ! empty( $ad->ID ) ) {
$this->set_id( absint( $ad->ID ) );
} else {
$this->set_object_read( true );
}
}
/* Getter ------------------- */
/**
* Get description.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return string
*/
public function get_description( $context = 'view' ): string {
return $this->get_prop( 'description', $context );
}
/**
* Get url.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return string
*/
public function get_url( $context = 'view' ): string {
return $this->get_prop( 'url', $context );
}
/**
* Get the expiry date of the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return int
*/
public function get_expiry_date( $context = 'view' ): int {
return $this->get_prop( 'expiry_date', $context );
}
/**
* Get the weekdays.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return array
*/
public function get_weekdays( $context = 'view' ): array {
return $this->get_prop( 'weekdays', $context );
}
/**
* Get the width of the ad in pixels.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return int
*/
public function get_width( $context = 'view' ): int {
return $this->get_prop( 'width', $context );
}
/**
* Get the height of the ad in pixels.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return int
*/
public function get_height( $context = 'view' ): int {
return $this->get_prop( 'height', $context );
}
/**
* Get the display conditions for the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return array
*/
public function get_display_conditions( $context = 'view' ): array {
return $this->get_prop( 'conditions', $context );
}
/**
* Get the visitor conditions for the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return array
*/
public function get_visitor_conditions( $context = 'view' ): array {
return array_filter(
$this->get_prop( 'visitors', $context ),
function ( $value ) {
return 'unknown' !== $value['type'];
}
);
}
/**
* Get clearfix.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return bool
*/
public function get_clearfix( $context = 'view' ): bool {
return $this->get_prop( 'clearfix', $context );
}
/**
* Get position.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return string
*/
public function get_position( $context = 'view' ): string {
return $this->get_prop( 'position', $context );
}
/**
* Get the margin setting for the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return array
*/
public function get_margin( $context = 'view' ): array {
return $this->get_prop( 'margin', $context );
}
/**
* Get the wrapper classes for the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return int
*/
public function get_weight( $context = 'view' ): int {
return $this->get_prop( 'weight', $context );
}
/**
* Retrieves the number of clicks for the ad.
*
* @return int
*/
public function get_clicks(): int {
$post = get_post();
if ( isset( $post->clicks ) ) {
return absint( $post->clicks );
}
global $wpdb;
$clicks = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
'SELECT SUM(count) FROM ' . $wpdb->prefix . 'advads_clicks WHERE ad_id = %d',
$this->get_id()
)
);
return absint( $clicks );
}
/**
* Retrieves the number of impressions for the ad.
*
* @return int
*/
public function get_impressions(): int {
$post = get_post();
if ( isset( $post->impressions ) ) {
return absint( $post->impressions );
}
global $wpdb;
$impressions = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->prepare(
'SELECT SUM(count) FROM ' . $wpdb->prefix . 'advads_impressions WHERE ad_id = %d',
$this->get_id()
)
);
return absint( $impressions );
}
/**
* Retrieves the number of ctr for the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return int|string
*/
public function get_ctr( $context = 'view' ) {
$post = get_post();
$ctr = $post->ctr ?? 0;
if ( ! $ctr && $this->get_impressions() > 0 ) {
$ctr = $this->get_clicks() / $this->get_impressions();
}
return 'view' === $context ? number_format_i18n( 100 * $ctr, 2 ) : $ctr;
}
/* Setter ------------------- */
/**
* Set display conditions.
*
* @param string $conditions Ad conditions.
*
* @return void
*/
public function set_display_conditions( $conditions ): void {
$this->set_prop( 'conditions', $conditions );
}
/**
* Set conditions.
*
* @param string $conditions Ad conditions.
*
* @return void
*/
public function set_visitor_conditions( $conditions ): void {
$this->set_prop( 'visitors', $conditions );
}
/**
* Set clearfix.
*
* @param string $clearfix Ad clearfix.
*
* @return void
*/
public function set_clearfix( $clearfix ): void {
$this->set_prop( 'clearfix', Formatting::string_to_bool( $clearfix ) );
}
/**
* Set position.
*
* @param string $position Ad position.
*
* @return void
*/
public function set_position( $position ): void {
$this->set_prop( 'position', empty( $position ) ? 'none' : $position );
}
/**
* Set margin.
*
* @param array $margin Ad margin.
*
* @return void
*/
public function set_margin( $margin ): void {
$margin = [
'top' => intval( $margin['top'] ?? 0 ),
'right' => intval( $margin['right'] ?? 0 ),
'bottom' => intval( $margin['bottom'] ?? 0 ),
'left' => intval( $margin['left'] ?? 0 ),
];
$this->set_prop( 'margin', $margin );
}
/**
* Set expiry date.
*
* @param array|string $expiry_date Ad expiry date.
*
* @return void
*/
public function set_expiry_date( $expiry_date ): void {
if ( is_array( $expiry_date ) ) {
$expiry_date = $this->normalize_expiry_date( $expiry_date );
}
$this->set_prop( 'expiry_date', $expiry_date );
}
/**
* Set weekdays.
*
* @param array $weekdays Ad weekdays.
*
* @return void
*/
public function set_weekdays( $weekdays ): void {
$this->set_prop( 'weekdays', $weekdays );
}
/**
* Set description.
*
* @param string $description Ad description.
*
* @return void
*/
public function set_description( $description ): void {
$this->set_prop( 'description', $description );
}
/**
* Set url.
*
* @param string $url Ad url.
*
* @return void
*/
public function set_url( $url ): void {
global $pagenow;
// If the tracking add-on is not active.
// If this is not the ad edit page.
if ( ! defined( 'AAT_VERSION' ) && ! in_array( $pagenow, [ 'post.php', 'post-new.php' ], true ) ) {
$placeholders = [
'[POST_ID]',
'[POST_SLUG]',
'[CAT_SLUG]',
'[AD_ID]',
];
$url = str_replace( $placeholders, '', $url );
}
$this->set_prop( 'url', trim( $url ) );
}
/**
* Set the width.
*
* @param float|string $width Total width.
*/
public function set_width( $width ) {
$this->set_prop( 'width', '' === $width ? 0 : absint( $width ) );
}
/**
* Set the height.
*
* @param float|string $height Total height.
*
* @return void
*/
public function set_height( $height ): void {
$this->set_prop( 'height', '' === $height ? 0 : absint( $height ) );
}
/**
* Set if debug mode is enabled.
*
* @param bool $enabled Is Enabled.
*
* @return void
*/
public function set_debugmode( $enabled ): void {
$this->set_prop( 'debugmode', Formatting::string_to_bool( $enabled ) );
}
/**
* Set if php is allowed.
*
* @param bool|string $allowed Allowed.
*
* @return void
*/
public function set_allow_php( $allowed ): void {
$this->set_prop( 'allow_php', Formatting::string_to_bool( $allowed ) );
}
/**
* Set if shortcodes is allowed.
*
* @param bool|string $allowed Allowed.
*
* @return void
*/
public function set_allow_shortcodes( $allowed ): void {
$this->set_prop( 'allow_shortcodes', Formatting::string_to_bool( $allowed ) );
}
/**
* Set add sizes is allowed.
*
* @param bool|string $allowed Allowed.
*
* @return void
*/
public function set_reserve_space( $allowed ): void {
$this->set_prop( 'reserve_space', Formatting::string_to_bool( $allowed ) );
}
/**
* Set if has weekdays.
*
* @param bool|string $has_weekdays Has weekdays.
*
* @return void
*/
public function set_has_weekdays( $has_weekdays ): void {
$this->set_prop( 'has_weekdays', Formatting::string_to_bool( $has_weekdays ) );
}
/* Conditional ------------------- */
/**
* Check if debug mode is enabled within the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return bool
*/
public function is_debug_mode( $context = 'view' ): bool {
return $this->get_prop( 'debugmode', $context );
}
/**
* Check if PHP code execution is allowed within the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return bool
*/
public function is_php_allowed( $context = 'view' ): bool {
return $this->get_prop( 'allow_php', $context );
}
/**
* Check if shortcode execution is allowed within the ad.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return bool
*/
public function is_shortcode_allowed( $context = 'view' ): bool {
return $this->get_prop( 'allow_shortcodes', $context );
}
/**
* Check if has weekdays.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return bool
*/
public function has_weekdays( $context = 'view' ): bool {
return $this->get_prop( 'has_weekdays', $context );
}
/**
* Check if space is reserved.
*
* @param string $context What the value is for. Valid values are view and edit.
*
* @return bool
*/
public function is_space_reserved( $context = 'view' ): bool {
return $this->get_prop( 'reserve_space', $context );
}
/**
* Check whether this ad is expired.
*
* @return bool
*/
public function is_expired(): bool {
if ( $this->get_expiry_date() <= 0 || $this->get_expiry_date() > time() ) {
return false;
}
return true;
}
/**
* Checks if the ad is placed in the header.
*
* @return bool
*/
public function is_head_placement(): bool {
if ( null !== $this->get_parent() && $this->get_parent()->is_type( 'header' ) ) {
return true;
}
// group as head placement.
if ( 'header' === $this->get_prop( 'group_placement_context' ) ) {
return true;
}
return false;
}
/**
* Check if the ad is a top-level ad.
*
* @return bool
*/
public function is_top_level(): bool {
return ! $this->get_parent() || is_a_placement( $this->get_parent() );
}
/**
* Determines whether the ad can be displayed.
*
* @param array $check_options check options.
*
* @return bool True if the ad can be displayed, false otherwise.
*/
public function can_display( $check_options = [] ): bool {
$check_options = wp_parse_args(
$check_options,
[
'passive_cache_busting' => false,
'ignore_debugmode' => false,
]
);
// Prevent ad to show up through wp_head, if this is not a header placement.
if ( doing_action( 'wp_head' ) && ! $this->is_head_placement() && ! Compatibility::can_inject_during_wp_head() ) {
return false;
}
// Check If the current ad is requested using a shortcode placed in the content of the current ad.
$shortcode_ad_id = $this->get_prop( 'shortcode_ad_id' );
if ( $shortcode_ad_id && absint( $shortcode_ad_id ) === $this->get_id() ) {
return false;
}
/**
* Allows bypassing server side visitor conditions checks or ignoring debug mode
*
* @param array $check_options can display check options.
*/
$check_options = apply_filters( 'advanced-ads-can-display-ad-check-options', $check_options );
// Force ad display if debug mode is enabled.
if ( $this->is_debug_mode() && ! $check_options['ignore_debugmode'] ) {
return true;
}
if ( ! $check_options['passive_cache_busting'] ) {
// Don’t display ads that are not published or private for users not logged in.
if ( ! $this->is_status( 'publish' ) && ! ( $this->is_status( 'private' ) && is_user_logged_in() ) ) {
return false;
}
if ( ! $this->can_display_by_visitor() ) {
return false;
}
} elseif ( ! $this->is_status( 'publish' ) ) {
return false;
}
if ( $this->is_expired() ) {
return false;
}
$can_display = apply_filters( 'advanced-ads-can-display-ad', true, $this, $check_options );
// Add own conditions to flag output as possible or not.
return apply_filters_deprecated( 'advanced-ads-can-display', [ $can_display, $this, $check_options ], '2.0.0', 'advanced-ads-can-display-ad', 'Use advanced-ads-can-display-ad instead.' );
}
/* Additional Methods ------------------- */
/**
* Prepares the output for the group.
*
* @return string The prepared output.
*/
public function prepare_output(): string {
$ad_args = $this->get_prop( 'ad_args' ) ?? [];
$output = $this->prepare_frontend_output();
// Don’t deliver anything, if main ad content is empty.
if ( empty( $output ) ) {
return '';
}
$output_options = $this->get_prop( 'output_options' ) ?? [];
$global_output = $output_options['global_output'] ?? ! isset( $ad_args['global_output'] ) || $ad_args['global_output'];
$this->set_prop_temp( 'global_output', $global_output );
$output_options['global_output'] = $global_output;
if ( ! $this->is_head_placement() ) {
// Filter to manipulate the output before the wrapper is added.
$output = apply_filters( 'advanced-ads-output-inside-wrapper', $output, $this );
$output = $this->add_wrapper( $output, $global_output );
// Add a clearfix, if set.
if (
( $this->is_top_level() && ! empty( $ad_args['placement_clearfix'] ) )
|| $this->get_clearfix()
) {
$output .= '
';
}
}
// Add the ad to the global output array.
if ( $output_options['global_output'] ) {
Stats::get()->add_entity( 'ad', $this->get_id(), $this->get_title() );
}
do_action_deprecated(
'advanced-ads-output',
[ $this, $output, $output_options ],
'2.0.0',
'advanced-ads-ad-output-ready',
'Use advanced-ads-ad-output-ready instead.'
);
return apply_filters_deprecated(
'advanced-ads-output-final',
[ $output, $this, $output_options ],
'2.0.0',
'advanced-ads-ad-output',
'Use advanced-ads-ad-output instead.'
);
}
/**
* Get groups
*
* @return bool|array
*/
public function get_groups() {
if ( null === $this->groups ) {
$this->groups = [];
$terms = wp_get_object_terms( $this->get_id(), Constants::TAXONOMY_GROUP );
if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
foreach ( $terms as $term ) {
$this->groups[ $term->term_id ] = wp_advads_get_group( $term );
}
}
}
return $this->groups;
}
/**
* Get ad type object
*
* @return Ad_Type|bool
*/
public function get_type_object() {
if ( ! wp_advads_has_ad_type( $this->get_type() ) ) {
wp_advads_create_ad_type( $this->get_type() );
}
return wp_advads_get_ad_type( $this->get_type() );
}
/**
* Get ad schedule details
*
* @return array
*/
public function get_ad_schedule_details(): array {
$status_strings = [];
$html_classes = 'advads-filter-timing';
$post_start = get_post_time( 'U', true, $this->get_id() );
$expiry_date_format = get_option( 'date_format' ) . ', ' . get_option( 'time_format' );
$status_type = get_post_status( $this->get_id() );
if ( empty( $status_type ) || 'publish' === $status_type ) {
$status_type = 'published';
}
if ( $post_start > time() ) {
$status_type = 'future';
$html_classes .= ' advads-filter-future';
/* translators: %s is a date. */
$status_strings[] = sprintf( __( 'starts %s', 'advanced-ads' ), get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $post_start ), $expiry_date_format ) );
}
$expiry = $this->get_expiry_date();
if ( ! empty( $expiry ) ) {
$html_classes .= ' advads-filter-any-exp-date';
$expiry_date = ( new \DateTimeImmutable( '@' . $expiry ) )->setTimezone( WordPress::get_timezone() );
$tz = ' (' . WordPress::get_timezone_name() . ')';
if ( $expiry > time() ) {
$status_type = 'expiring';
/* translators: %s is a date. */
$status_strings[] = sprintf( __( 'expires %s', 'advanced-ads' ), $expiry_date->format( $expiry_date_format ) ) . $tz;
} else {
$status_type = 'expired';
$html_classes .= ' advads-filter-expired';
/* translators: %s is a date. */
$status_strings[] = sprintf( __( 'expired %s', 'advanced-ads' ), $expiry_date->format( $expiry_date_format ) ) . $tz;
}
}
$labels = [
'published' => __( 'Published', 'advanced-ads' ),
'draft' => __( 'Draft', 'advanced-ads' ),
'trash' => __( 'Trashed', 'advanced-ads' ),
];
if ( isset( $labels[ $status_type ] ) ) {
$status_strings[] = $labels[ $status_type ];
}
return compact( 'post_start', 'status_type', 'status_strings', 'html_classes' );
}
/**
* Load a template with the information on ad expiry
*
* @return string
*/
public function get_ad_schedule_html(): string {
[
'post_start' => $post_start,
'status_type' => $status_type,
'status_strings' => $status_strings
] = $this->get_ad_schedule_details();
if ( empty( $status_strings ) ) {
return '';
}
ob_start();
include ADVADS_ABSPATH . 'views/admin/tables/ads/icon-status.php';
return ob_get_clean();
}
/**
* Get the post edit link
*
* @return string|null
*/
public function get_edit_link() {
return get_edit_post_link( $this->get_id(), 'other' );
}
/**
* Normalize expiry date
*
* @param array $expiry_date Expiry date.
*
* @return int
*/
private function normalize_expiry_date( $expiry_date ): int {
// Early bail!!
if ( empty( $expiry_date['enabled'] ) ) {
return 0;
}
$local_string = sprintf(
'%04d-%02d-%02d %02d:%02d:00',
absint( $expiry_date['year'] ),
absint( $expiry_date['month'] ),
absint( $expiry_date['day'] ),
absint( $expiry_date['hour'] ),
absint( $expiry_date['minute'] )
);
$local_dt = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $local_string, WordPress::get_timezone() );
return $local_dt ? $local_dt->getTimestamp() : 0;
}
/**
* Process shortcodes.
*
* @param string $output Ad content.
*
* @return string
*/
protected function do_shortcode( $output ): string {
$ad_args = $this->temp_data;
$ad_args['shortcode_ad_id'] = $this->get_id();
$output = preg_replace(
'/\[(the_ad_group|the_ad_placement|the_ad) /',
'[$1 ad_args="' . rawurlencode( wp_json_encode( $ad_args ) ) . '" ',
$output
);
return do_shortcode( $output );
}
/**
* Add a wrapper arount the ad content if wrapper information are given
*
* @since 1.1.4
*
* @param string $ad_content content of the ad.
* @param bool $global_output Global output.
*
* @return string $wrapper ad within the wrapper
*/
private function add_wrapper( $ad_content = '', $global_output = false ): string {
$ad_args = $this->get_prop( 'ad_args' ) ?? [];
$label = $this->get_label();
$wrapper = $this->create_wrapper();
$wrapper_options = apply_filters( 'advanced-ads-output-wrapper-options', $wrapper, $this );
// Create another wrapper so that the label does not reduce the height of the ad wrapper.
if ( $label && ! empty( $wrapper_options['style']['height'] ) ) {
$height = [ 'style' => [ 'height' => $wrapper_options['style']['height'] ] ];
unset( $wrapper_options['style']['height'] );
$ad_content = '