*/ use AdvancedAds\Utilities\Conditional; use AdvancedAds\Framework\Utilities\Params; use GeoIp2\Exception\AddressNotFoundException; /** * WP Admin class for Geo Targeting. */ class Advanced_Ads_Geo_Admin { /** * Path to view files * * @var string */ private $views_path; /** * Initialize the plugin by loading admin scripts & styles and adding a * settings page and menu. * * @since 1.0.0 */ public function __construct() { $this->views_path = plugin_dir_path( __FILE__ ) . '../admin/views'; add_action( 'advanced-ads-settings-init', [ $this, 'settings_init' ] ); // ajax request to download the database. add_action( 'wp_ajax_advads_download_geolite_database', [ $this, 'download_database' ] ); // Add assets. add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] ); } /** * Add settings to settings page */ public function settings_init() { // add new section. add_settings_section( 'advanced_ads_geo_setting_section', __( 'Geo Targeting', 'advanced-ads-pro' ), '__return_empty_string', Advanced_Ads_Pro::OPTION_KEY . '-settings' ); // Add license key field. add_settings_field( 'geo-maxmind-license-key', __( 'MaxMind license key', 'advanced-ads-pro' ), [ $this, 'render_settings_license_key_callback' ], Advanced_Ads_Pro::OPTION_KEY . '-settings', 'advanced_ads_geo_setting_section' ); // Add database field. add_settings_field( 'geo-database-update', __( 'MaxMind Database', 'advanced-ads-pro' ), [ $this, 'render_settings_database' ], Advanced_Ads_Pro::OPTION_KEY . '-settings', 'advanced_ads_geo_setting_section' ); // add field for the targeting method. if ( count( Advanced_Ads_Geo_Plugin::get_instance()->get_targeting_methods() ) > 1 ) { add_settings_field( 'geo-license', __( 'Method', 'advanced-ads-pro' ), [ $this, 'render_settings_method_callback' ], Advanced_Ads_Pro::OPTION_KEY . '-settings', 'advanced_ads_geo_setting_section' ); } // add assistant setting field. add_settings_field( 'geo-locale', __( 'Language of names', 'advanced-ads-pro' ), [ $this, 'render_settings_locale_option_callback' ], Advanced_Ads_Pro::OPTION_KEY . '-settings', 'advanced_ads_geo_setting_section' ); } /** * Render MaxMind license key field. */ public function render_settings_license_key_callback() { $use_filters = $this->is_using_custom_database(); $license_key = Advanced_Ads_Geo_Plugin::get_instance()->options( 'maxmind-license-key', '' ); include $this->views_path . '/setting-maxmind-license-key.php'; } /** * Render MaxMind database field. * * @return void */ public function render_settings_database() { $last_update = get_option( ADVADS_SLUG . '-' . Advanced_Ads_Geo_Plugin::OPTIONS_SLUG . '-last-update-geolite2', false ); $next_update = $this->get_next_first_tuesday_timestamp(); // Check if the database files exist and do not contain errors. $api = Advanced_Ads_Geo_Api::get_instance(); $correct_databases = $api->get_geo_ip2_city_reader() && $api->get_geo_ip2_country_reader(); $use_filters = $this->is_using_custom_database(); // Render download of the geo database. include $this->views_path . '/setting-download.php'; } /** * Render option for the geo targeting method */ public function render_settings_method_callback() { $method = Advanced_Ads_Geo_Plugin::get_instance()->options( 'method', 'default' ); $methods = Advanced_Ads_Geo_Plugin::get_instance()->get_targeting_methods(); include $this->views_path . '/setting-method.php'; } /** * Render option for language of the geo information */ public function render_settings_locale_option_callback() { $locale = Advanced_Ads_Geo_Plugin::get_instance()->options( 'locale', 'en' ); include $this->views_path . '/setting-locale.php'; } /** * Allow static method call as visitor condition callback. * * @param string $name method name. * @param mixed $arguments functions arguments. */ public static function __callStatic( $name, $arguments ) { if ( 'metabox_geo' === $name ) { ( new self() )->metabox_geo( ...$arguments ); } // else fail silently. } /** * Add visitor condition box * * @param array $options ad options. * @param int $index current index in conditions list. * @param string $form_name form name of current condition. * * @return void * @throws \MaxMind\Db\Reader\InvalidDatabaseException Corrupted DB files. */ private function metabox_geo( $options, $index = 0, $form_name = '' ) { if ( empty( $options['type'] ) ) { return; } $type_options = Advanced_Ads_Visitor_Conditions::get_instance()->conditions; if ( ! isset( $type_options[ $options['type'] ] ) ) { return; } $method = Advanced_Ads_Geo_Plugin::get_current_targeting_method(); $countries = Advanced_Ads_Geo_Api::get_countries(); // set defaults for all variables. $my_country = ''; $my_country_iso_code = ''; $my_city = ''; $my_region = ''; $my_lat = 0.0; $my_lon = 0.0; switch ( $method ) { case 'sucuri': $my_country_iso_code = Advanced_Ads_Geo_Plugin::get_sucuri_country(); $my_country = $countries[ $my_country_iso_code ] ?? ' – '; $current_location = sprintf( '%s (%s)', $my_country, $my_country_iso_code ); break; default: // get information from the current user to help him debugging issues. $api = Advanced_Ads_Geo_Api::get_instance(); $ip = $api->get_real_ip_address(); $error = false; // get locale. $locale = Advanced_Ads_Geo_Plugin::get_instance()->options( 'locale', 'en' ); if ( $ip ) { try { $reader = $api->get_geo_ip2_city_reader(); if ( $reader ) { // Look up the IP address. $record = $reader->city( $ip ); if ( ! empty( $record ) ) { $my_city = ( $record->city->name ) ? $record->city->name : __( '(unknown city)', 'advanced-ads-pro' ); if ( isset( $record->city->names[ $locale ] ) && $record->city->names[ $locale ] ) { $my_city = $record->city->names[ $locale ]; } $my_country = ( isset( $record->country->names[ $locale ] ) && $record->country->names[ $locale ] ) ? $record->country->names[ $locale ] : $record->country->names['en']; $my_country_iso_code = $record->country->isoCode; // get first subdivision (region/state). $my_region = ( isset( $record->subdivisions[0] ) && $record->subdivisions[0]->name ) ? $record->subdivisions[0]->name : __( '(unknown region)', 'advanced-ads-pro' ); if ( isset( $record->subdivisions[0] ) && isset( $record->subdivisions[0]->names[ $locale ] ) && $record->subdivisions[0]->names[ $locale ] ) { $my_region = $record->subdivisions[0]->names[ $locale ]; } if ( isset( $record->location ) && isset( $record->location->latitude ) && isset( $record->location->longitude ) ) { $my_lat = $record->location->latitude; $my_lon = $record->location->longitude; } } } else { $error = sprintf( '%1$s %2$s', __( 'Geo Databases not found.', 'advanced-ads-pro' ), sprintf( /* translators: 1: opening -tag to Advanced Ads manual, 2: closing -tag */ __( 'Please read the %1$sinstallation instructions%2$s.', 'advanced-ads-pro' ), '', '' ) ); } } catch ( AddressNotFoundException $e ) { $error = $e->getMessage() . ' ' . __( 'Maybe you are working on a local or secured environment.', 'advanced-ads-pro' ); } } else { $raw_ip = $api->get_raw_ip_address(); $error = '' . __( 'Your IP address format is incorrect', 'advanced-ads-pro' ) . ' (' . $raw_ip . ')'; } if ( $error ) { $current_location = '' . $error . ''; } else { $current_location = $ip . ', ' . $my_country . ', ' . $my_region . ', ' . $my_city; $current_location .= '
' . __( 'Coordinates', 'advanced-ads-pro' ) . ': (' . $my_lat . ' / ' . $my_lon . ')'; } } // form name basis. if ( method_exists( 'Advanced_Ads_Visitor_Conditions', 'get_form_name_with_index' ) ) { $name = Advanced_Ads_Visitor_Conditions::get_form_name_with_index( $form_name, $index ); } else { $name = Advanced_Ads_Visitor_Conditions::FORM_NAME . '[' . $index . ']'; } $operator = isset( $options['operator'] ) ? $options['operator'] : 'is'; $geo_mode = isset( $options['geo_mode'] ) ? $options['geo_mode'] : 'classic'; ?>
value="classic" onclick="advads_geo_admin.set_mode(, this.value);"> value="latlon" onclick="advads_geo_admin.set_mode(, this.value);">

:
[], 'span' => [ 'class' => true ], 'a' => [ 'href' => true, 'target' => true, ], ] ); ?>

render_visitor_profile_information( $my_lat, $my_lon ); ?>
get_upload_full(); if ( ! $upload_full ) { wp_send_json_error( __( 'The upload dir is not available', 'advanced-ads-pro' ), 400 ); } $license_key = sanitize_text_field( wp_unslash( Params::request( 'license_key', '' ) ) ); if ( empty( $license_key ) ) { wp_send_json_error( __( 'Please provide a MaxMind license key', 'advanced-ads-pro' ), 400 ); } $file_prefix = Advanced_Ads_Geo_Plugin::get_maxmind_file_prefix(); if ( '' === $file_prefix ) { $file_prefix = $this->create_new_prefix(); $this->remove_obsolete_files(); } // download source. $download_urls = [ 'city' => 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=' . $license_key, 'country' => 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&suffix=tar.gz&license_key=' . $license_key, ]; // Paths where we will save files. $filepaths = [ 'city' => $upload_full . $file_prefix . '-GeoLite2-City.mmdb', 'country' => $upload_full . $file_prefix . '-GeoLite2-Country.mmdb', ]; // Names in the `tar.gz` archives. $filenames_in_archive = [ 'city' => 'GeoLite2-City.mmdb', 'country' => 'GeoLite2-Country.mmdb', ]; // create upload directory if not exists yet. if ( ! file_exists( $upload_full ) ) { // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_mkdir mkdir( $upload_full ); // phpcs:enable } // Prevent directory listing. if ( ! file_exists( $upload_full . 'index.html' ) ) { // phpcs:disable WordPress.WP.AlternativeFunctions,WordPress.PHP.NoSilencedErrors.Discouraged $handle = @fopen( $upload_full . 'index.html', 'w' ); fwrite( $handle, '' ); fclose( $handle ); // phpcs:enable } if ( function_exists( 'wp_raise_memory_limit' ) ) { wp_raise_memory_limit(); } $this->maybe_replace_tmp_dir(); foreach ( $download_urls as $key => $download_url ) { // variable with the name of the database file to download. $db_file = $filepaths[ $key ]; $filename = $filenames_in_archive[ $key ]; $result = $this->download_geolite2_database( $download_url, $db_file, $filename ); if ( ! isset( $result['state'] ) || ! $result['state'] ) { wp_send_json_error( $result['message'], 400 ); } } // save geo options on db update. $pro_plugin = Advanced_Ads_Pro::get_instance(); $pro_options = $pro_plugin->get_options(); if ( ! array_key_exists( Advanced_Ads_Geo_Plugin::OPTIONS_SLUG, $pro_options ) ) { $pro_options[ Advanced_Ads_Geo_Plugin::OPTIONS_SLUG ] = []; } $pro_options[ Advanced_Ads_Geo_Plugin::OPTIONS_SLUG ]['maxmind-license-key'] = $license_key; $pro_options[ Advanced_Ads_Geo_Plugin::OPTIONS_SLUG ]['locale'] = sanitize_text_field( wp_unslash( Params::request( 'locale', '' ) ) ); $pro_plugin->update_options( $pro_options ); update_option( ADVADS_SLUG . '-' . Advanced_Ads_Geo_Plugin::OPTIONS_SLUG . '-last-update-geolite2', time() ); wp_send_json_success( __( 'Database updated successfully!', 'advanced-ads-pro' ) ); } /** * Download GeoLite2 databases * * @param string $download_url The download url. * @param string $db_file The target file. * @param string $filename The target filename in the archive. * * @return array */ private function download_geolite2_database( $download_url, $db_file, $filename ) { // download the file from MaxMind, this places into temporary location. $temp_file = download_url( $download_url ); $result['state'] = false; $result['message'] = __( 'Database update failed', 'advanced-ads-pro' ); // If we failed, through a message, otherwise proceed. if ( is_wp_error( $temp_file ) ) { $error_code = $temp_file->get_error_data()['code'] ?? ''; $error_body = $temp_file->get_error_data()['body'] ?? ''; $result['message'] = sprintf( /* translators: 1: The download URL, 2: The HTTP error code, 3: The HTTP header title, 4: The response body */ __( 'Error downloading database from: %1$s - %2$s %3$s - %4$s', 'advanced-ads-pro' ), wp_parse_url( $download_url, PHP_URL_HOST ), // Do not expose the license key from query string. $error_code, $temp_file->get_error_message(), $error_body ); error_log( 'Advanced Ads Geo: ' . $result['message'] ); // phpcs:ignore } else { // The dir where the archive was downloaded. $temp_file_dir = dirname( $temp_file ); try { $archive = new PharData( $temp_file ); // The dir extracted from the archive. $extracted_dir = trailingslashit( $archive->current()->getFilename() ); // Full path to the dir extracted from the archive. $extracted_dir_full = trailingslashit( $temp_file_dir ) . $extracted_dir; $archive->extractTo( $temp_file_dir, $extracted_dir . $filename, true ); } catch ( Exception $e ) { $result['message'] = sprintf( /* translators: an error mesage. */ esc_html__( 'Could not open downloaded database for reading: %s', 'advanced-ads-pro' ), $temp_file . ', ' . $e->getMessage() ); error_log( 'Advanced Ads Geo: ' . $result['message'] ); // phpcs:ignore return $result; } finally { wp_delete_file( $temp_file ); } add_filter( 'filesystem_method', [ $this, 'set_direct_filesystem_method' ] ); WP_Filesystem(); global $wp_filesystem; if ( ! isset( $wp_filesystem->method ) || 'direct' !== $wp_filesystem->method ) { $result['message'] = __( 'Could not access filesystem', 'advanced-ads-pro' ); error_log( 'Advanced Ads Geo: ' . $result['message'] ); // phpcs:ignore return $result; } // Move the file. $renamed = $wp_filesystem->move( $extracted_dir_full . $filename, $db_file, true ); $wp_filesystem->delete( $extracted_dir_full, false, 'd' ); if ( ! $renamed ) { /* translators: MaxMind database file name. */ $result['message'] = sprintf( __( 'Could not open database for writing %s', 'advanced-ads-pro' ), $db_file ); // Remove corrupted file. $wp_filesystem->delete( $db_file, false, 'f' ); } else { $result['message'] = ''; $result['state'] = true; } remove_filter( 'filesystem_method', [ $this, 'set_direct_filesystem_method' ] ); } return $result; } /** * Get timestamp of the next first Tuesday of a month (either this month or the next) * * @param string $time timestamp from which to get the next available Tuesday. * * @return int of the next Tuesday (midnight GMT) * @since 1.0.0 */ public function get_next_first_tuesday_timestamp( $time = 0 ) { // we actually use Wednesday since it returns midnight. if ( ! $time ) { $time = time(); } // current month. // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date $month = date( 'F', $time ); $year = date( 'Y', $time ); $next_tuesday_t = strtotime( "first Wednesday of $month $year" ); // if this is the past, get first Tuesday of next month. if ( $next_tuesday_t < time() ) { $next_month_t = strtotime( 'next month' ); $month = date( 'F', $next_month_t ); $year = date( 'Y', $next_month_t ); $next_tuesday_t = strtotime( "first Tuesday of $month $year" ); } // phpcs:enable return $next_tuesday_t; } /** * Check if the databases are working */ public function check_database() { $api = Advanced_Ads_Geo_Api::get_instance(); return $api->get_geo_lite_country_filename() && $api->get_geo_lite_city_filename(); } /** * Check if an updated version of the database might already be available * we use the timestamp of the last update that actually happened and check, * if the next first Tuesday of a month that followed is already in the past * * @return bool true, if update should be available * @since 1.0.0 */ public function is_update_available() { // check if database is available. if ( ! $this->check_database() ) { return true; } // current time. $now_t = time(); // get last update from the database. $last_update_t = get_option( ADVADS_SLUG . '-' . Advanced_Ads_Geo_Plugin::OPTIONS_SLUG . '-last-update-geolite2', $now_t ); // return true, if last update is more than 31 days ago. $month_in_seconds = 31 * DAY_IN_SECONDS; if ( $month_in_seconds <= $now_t - $last_update_t ) { return true; } // get next Tuesday following the update. $next_tuesday_t = $this->get_next_first_tuesday_timestamp( $last_update_t ); if ( $next_tuesday_t < $now_t ) { return true; } return false; } /** * Check if license is valid * * @return bool true if license is valid * @since 1.0.0 */ public function license_valid() { $status = Advanced_Ads_Admin_Licenses::get_instance()->get_license_status( 'advanced-ads-geo' ); if ( 'valid' === $status ) { return true; } return false; } /** * Register and enqueue admin-specific scripts. * * @return void */ public function enqueue_admin_scripts() { if ( ! Conditional::is_screen_advanced_ads() ) { return; } $handle = ADVADS_SLUG . '-' . Advanced_Ads_Geo_Plugin::OPTIONS_SLUG . '-admin-script'; wp_enqueue_script( $handle, plugin_dir_url( __FILE__ ) . '../admin/assets/admin.js', [], AAP_VERSION, true ); wp_localize_script( $handle, 'advads_geo_translation', [ /* translators: 1: The number of search results. */ 'found_results' => __( 'Found %1$d results. Please pick the one, you want to use.', 'advanced-ads-pro' ), 'no_results' => __( 'Your search did not return any results.', 'advanced-ads-pro' ), 'could_not_retrieve_city' => __( 'There was an error connecting to the search service.', 'advanced-ads-pro' ), /* translators: 1: A link to the geo location service. */ 'manual_geo_search' => sprintf( __( 'You can search for the geo coordinates manually at %1$s.', 'advanced-ads-pro' ), 'nominatim.openstreetmap.org' ), 'COOKIEPATH' => COOKIEPATH, 'COOKIE_DOMAIN' => COOKIE_DOMAIN, 'nonce' => wp_create_nonce( 'advanced-ads-admin-ajax-nonce' ), ] ); } /** * As per MaxMind TOS, databases should not be puclicly accessible. Create a random prefix for that. */ private function create_new_prefix() { $new_prefix = wp_generate_password( 32, false ); update_option( 'advanced-ads-geo-maxmind-file-prefix', $new_prefix ); return $new_prefix; } /** * Remove files prefixed with an obsolete prefix. */ public function remove_obsolete_files() { require_once ABSPATH . 'wp-admin/includes/file.php'; add_filter( 'filesystem_method', [ $this, 'set_direct_filesystem_method' ] ); WP_Filesystem(); global $wp_filesystem; if ( ! isset( $wp_filesystem->method ) || 'direct' !== $wp_filesystem->method ) { return; } $upload_full = Advanced_Ads_Geo_Plugin::get_instance()->get_upload_full(); if ( ! $upload_full ) { return; } $files = $wp_filesystem->dirlist( $upload_full ); if ( ! $files ) { return; } foreach ( $files as $file ) { if ( preg_match( '/GeoLite2-.*?\.mmdb$/', $file['name'] ) || 'tmp-' === substr( $file['name'], 0, 4 ) ) { $wp_filesystem->delete( $upload_full . $file['name'] ); } } remove_filter( 'filesystem_method', [ $this, 'set_direct_filesystem_method' ] ); } /** * Set direct filesystem method. */ public function set_direct_filesystem_method() { return 'direct'; } /** * Create a new tmp dir inside the upload dir if the existing one does not contain enought space. */ private function maybe_replace_tmp_dir() { if ( defined( 'WP_TEMP_DIR' ) ) { return; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged $available_tmp_space = @disk_free_space( get_temp_dir() ); // We need > 200 MB. if ( ! $available_tmp_space || ( $available_tmp_space / MB_IN_BYTES ) < 200 ) { $tmp_dir = Advanced_Ads_Geo_Plugin::get_instance()->get_upload_full() . 'tmp-' . Advanced_Ads_Geo_Plugin::get_maxmind_file_prefix(); if ( ! file_exists( $tmp_dir ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir,WordPress.PHP.NoSilencedErrors.Discouraged @mkdir( $tmp_dir ); } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( @is_dir( $tmp_dir ) && wp_is_writable( $tmp_dir ) ) { define( 'WP_TEMP_DIR', $tmp_dir ); } } } /** * Render a table with the current visitor profile cookie information if available. * * @param float $lat User's latitude. * @param float $lon User's longitude. * * @return void */ private function render_visitor_profile_information( $lat, $lon ) { $visitor_profile = new Advanced_Ads_Geo_Visitor_Profile(); if ( $visitor_profile->has_visitor_profile && $visitor_profile->is_different_from_current_location( $lat, $lon ) ) { require $this->views_path . '/visitor-profile.php'; } } /** * Check if custom database files are used via filters * * @return bool */ private function is_using_custom_database() { $api = Advanced_Ads_Geo_Api::get_instance(); return has_filter( 'advanced-ads-geo-maxmind-geolite2-country-db-filepath' ) && has_filter( 'advanced-ads-geo-maxmind-geolite2-city-db-filepath' ) && $api->get_geo_ip2_city_reader() && $api->get_geo_ip2_country_reader(); } }