registerAdLocation($location, $label) - Register a custom ad location (relevant within themes) */ require dirname(__FILE__).'/ad-types.php'; /* * AdManager Class * * @since 1.0 */ class AdManager{ const VERSION = '0.9.3', // plugin version ID = 'ad_manager', // internally used, mostly for text domain PROJECT_URI = 'http://digitalnature.eu/forum/plugins/ad-manager/'; // plugin project page protected static $instance = null; protected // current plugin options, includes ads $options = null, // custom ad locations registered by themes or plugins $theme_ad_locations = array(), // holds registered ad types $ad_types = array(), // for locations that require post / comment index $location_index_conditions = array(), // before / after / half position records for default locations $default_location_positions = array(), // for locations that require specific pages $location_page_conditions = array(), // visible ads queue $queue = array(), // default option values $defaults = array( // version for the options, needed during updates 'version' => self::VERSION, // we're storing the ads here; no need for a custom db table, // because there shouldn't be that many ads on a typical site... 'data' => array(), ); /* * This will instantiate the class if needed, and return the only class instance if not... * * @since 1.0 */ public static function app(){ // first run? if(!(self::$instance instanceof self)){ self::$instance = new self(); // localize load_plugin_textdomain(self::ID, false, dirname(plugin_basename(__FILE__)).'/lang'); // admin hooks if(is_admin()){ add_action('admin_menu', array(self::$instance, 'createMenu')); add_action('admin_init', array(self::$instance, 'registerSettings')); add_action('wp_ajax_ad_form', array(self::$instance, 'adForm')); add_action('wp_ajax_process_ad', array(self::$instance, 'processAd')); add_action('wp_ajax_change_ad_type', array(self::$instance, 'changeAdType')); add_action('wp_ajax_scan_type_html', array(self::$instance, 'scanTypeHTML')); add_action('wp_ajax_get_ad_stats', array(self::$instance, 'getAdStats')); // front-end hooks }else{ add_action('wp', array(self::$instance, 'run')); add_action('init', array(self::$instance, 'trackAds')); // register [ad] shortcode add_shortcode('ad', array(self::$instance, 'shortcode')); } // register widget add_action('widgets_init', array(self::$instance, 'widget')); // run on plugin uninstall; not sure when does this run?! register_uninstall_hook(__FILE__, array(__CLASS__, 'uninstall')); } return self::$instance; } /* * A single instance only * * @since 1.0 */ final protected function __construct(){ // first call initializes plugin options $this->getOptions(); $this->registerAdType('AdHTML'); $this->registerAdType('AdImageLink'); //$this->registerAdType('AdAdSense'); // @todo } /* * No cloning * * @since 1.0 */ final protected function __clone(){} /* * Returns one or all plugin options. * * @since 1.0 * @param string $key Option to get; if not given all options are returned * @return mixed Option(s) */ public function getOptions($key = false){ // first call, initialize the options if(!isset($this->options)){ $options = get_option(self::ID); // options exist if($options !== false){ $new_version = version_compare($options['version'], self::VERSION, '!='); $desync = array_diff_key($this->defaults, $options) !== array_diff_key($options, $this->defaults); // update options if version changed, or we have missing/extra (out of sync) option entries if($new_version || $desync){ $new_options = array(); // check for new options and set defaults if necessary foreach($this->defaults as $option => $value) $new_options[$option] = isset($options[$option]) ? $options[$option] : $value; // update version info $new_options['version'] = self::VERSION; update_option(self::ID, $new_options); $this->options = $new_options; // no update was required }else{ $this->options = $options; } // new install (plugin was just activated) }else{ update_option(self::ID, $this->defaults); $this->options = $this->defaults; } } return $key ? $this->options[$key] : $this->options; } /* * Set current options * * @since 1.0 * @param array $options Options */ public function setOptions($options){ update_option(self::ID, $options); $this->options = $options; } /* * Loads a template file from the theme or child theme directory. * * @since 1.0 * @param string $_name Template name, without the '.php' suffix * @param array $_vars Variables to expose in the template. Note that unlike WP, we're not exposing all the global variable mess inside it... */ final public function loadTemplate($_name, $_vars = array()){ // you cannot let locate_template to load your template // because WP devs made sure you can't pass // variables to your template :( $_located = locate_template($_name, false, false); // use the default one if the (child) theme doesn't have it if(!$_located) $_located = dirname(__FILE__).'/templates/'.$_name.'.php'; unset($_name); // create variables if($_vars) extract($_vars); // load it require $_located; } /* * Hook our plugin options menu / page * * @since 1.0 */ public function createMenu(){ $page = add_options_page(__('Ad Manager', self::ID), __('Ad Manager', self::ID), 'manage_options', self::ID, array($this, 'SettingsPage')); add_action("admin_print_scripts-{$page}", array($this, 'assets')); } /* * Register our setting with the new useless Settings API bloat... * * @since 1.0 */ public function registerSettings(){ add_filter('plugin_action_links', array($this, 'pluginSettingsLink'), 10, 2); } /* * Settings link in the plugin list * * @since 1.0 * @param string $file * @param array $links * @return array */ public function pluginSettingsLink($links, $file){ if(plugin_basename(__FILE__) === $file){ $settings_link = ''.__('Manage Ads', self::ID).''; array_unshift($links, $settings_link); } return $links; } /* * Get currently registered ads * * @since 1.0 * @return array */ public function getRegisteredAds($id = false){ $data = $this->getOptions('data'); if($id !== false) return isset($data[$id]) ? $data[$id] : false; return $data; } /* * Replaces all ad data. * * @since 1.0 * @param array $data New data */ public function updateRegisteredAds($data){ $options = $this->getOptions(); $options['data'] = $data; $this->setOptions($options); } /* * Update properties of a specific ad. * If $key_or_data is an array, all existing properties will be replaced with this value, * otherwise this is considered a key, and $value will be used as the key's value. * * @since 1.0 * @param int $id Ad ID * @param string|array|bool $key_or_data Property to update, or the values of all properties as an array; if "false", the ad will be removed * @param mixed Optional, new value of the property (if a key was given) * @return bool */ public function updateRegisteredAd($id, $key_or_data, $value = ''){ $options = $this->getOptions(); // ad doesn't exist; we should never reach this point if(!isset($options['data'][$id])) return false; if(is_array($key_or_data)) $options['data'][$id] = $key_or_data; elseif($key_or_data !== false) $options['data'][$id][$key_or_data] = $value; else unset($options['data'][$id]); $this->setOptions($options); return true; } /* * Allow other plugins/themes to register new ad types. * You must provide the class name as argument * * @since 1.0 * @param string $type */ public function registerAdType($type){ $object = new $type; if(!($object instanceof AdManagerType)) throw new Exception('The class must be a child of AdManagerType!'); $this->ad_types[$type] = $object; } /* * Allow themes to register ad locations. * Atom themes will do this, and hopefully other themes will too ;) * * @since 1.0 * @param string $id * @param string $label * @param array $pages */ public function registerAdLocation($id, $label = '', $pages = array()){ // can register multiple locations at once if first argument is an array $locations = is_array($id) ? $id : array($id => $label); foreach($locations as $id => $label){ $this->theme_ad_locations["theme:{$id}"] = $label ? $label : $id; $this->location_page_conditions["theme:{$id}"] = $pages; } } /* * Get a list of valid ad locations. * * @since 1.0 * @param string $single Only retrieve locations of this type * @return array */ protected function getAdLocations($single = false){ $locations = apply_filters('ad_manager_locations', array( // locations registered by themes (or other plugins) 'theme' => $this->theme_ad_locations, // built-in locations // p: and c: denote post / comment context 'defaults' => array( 'p:before_post:index' => __('Prepend to post content', self::ID), 'p:half_post:index' => __('Half-way trough post content (experimental)', self::ID), 'p:after_post:index' => __('Append to post content', self::ID), 'c:before_comment:index' => __('Prepend to comment content', self::ID), 'c:half_comment:index' => __('Half-way trough comment content (experimental)', self::ID), 'c:after_comment:index' => __('Append to comment content', self::ID), ), 'other' => array( 'shortcode' => __('Use shortcode', self::ID), 'action' => __('Custom action', self::ID), ), )); if(!$single) return $locations; foreach($locations as $group) foreach($group as $location => $label) if($location === $single) return $label; } /* * Get a list of valid ad types. * * @since 1.0 * @return array */ protected function getAdTypes(){ $types = array(); foreach($this->ad_types as $id => $object) $types[$id] = $object->getLabel(); return $types; } /* * Get a list of valid page contexts. * * @since 1.0 * @return array */ protected function getAdPages($single = false){ $pages = array( 'any' => __('Auto (any page with this location)', self::ID), 'home' => __('Blog home', self::ID), ); foreach(get_post_types(array('public' => true)) as $post_type){ $object = get_post_type_object($post_type); if(empty($object->labels->name)) continue; $pages["singular:{$post_type}"] = sprintf(__('Single: %s', self::ID), $object->labels->name); } $pages['category'] = __('Category archives', self::ID); $pages['tag'] = __('Tag archives', self::ID); $pages['author'] = __('Author archives', self::ID); $pages['date'] = __('Date-based archives', self::ID); $pages['search'] = __('Search results', self::ID); foreach(get_taxonomies(array('public' => true, '_builtin' => false)) as $taxonomy){ $object = get_taxonomy($taxonomy); if(empty($object->labels->name)) continue; $pages["tax:{$taxonomy}"] = sprintf(__('Taxonomy archive: %s', self::ID), $object->labels->name); } $pages = apply_filters('ad_manager_pages', $pages); if($single) return isset($pages[$single]) ? $pages[$single] : ''; return $pages; } /* * Get a list of valid user class contexts. * * @since 1.0 * @return array */ protected function getAdUsers($single = false){ $users = array( 'anyone' => __('To anyone', self::ID), 'visitors' => __('Only to visitors (unregistered users)', self::ID), 'no-admin' => __('To all but administrators', self::ID), ); $wp_roles = new WP_Roles(); foreach($wp_roles->get_names() as $role => $label) $users["role:{$role}"] = sprintf(__('By role: %s', self::ID), translate_user_role($label)); $users = apply_filters('ad_manager_users', $users); if(!$single) return $users; foreach($users as $context => $label) if($context === $single) return $label; } /* * Records simple statistics from links that are set to be tracked. * * @since 1.0 */ public function trackAds(){ // not our request if(!isset($_GET['adtrack'])) return; $id = (int)$_GET['adtrack']; $ad = $this->getRegisteredAds($id); // ad doesn't exist, return if(!$ad || !$ad['track']) return; // increase click counter $this->updateRegisteredAd($id, 'clicks', $ad['clicks'] + 1); $stats = get_transient("ad_manager_stats_{$id}"); if($stats === false) $stats = array(); $ip = $_SERVER['REMOTE_ADDR']; $stats[] = array( 'ip' => $ip, 'time' => current_time('timestamp', true), 'user' => is_user_logged_in() ? wp_get_current_user()->ID : 0, 'ref' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', ); // 10 entries max. while(count($stats) > 10) array_shift($stats); // stats will get deleted in two weeks if there's no activity set_transient("ad_manager_stats_{$id}", $stats, 60 * 60 * 24 * 14); wp_redirect($ad['link']); exit; } /* * Displays click stats for an ad * * @since 1.0 */ public function getAdStats(){ if(!current_user_can('manage_options')) return; $id = (int)$_GET['id']; $stats = get_transient("ad_manager_stats_{$id}"); ?>
getRegisteredAds($id); if(!$data) return; extract($data, EXTR_SKIP); $tr_classes = array(); if(!$active) $tr_classes[] = 'inactive'; if($id % 2 === 0) $tr_classes[] = 'alternate'; $tr_classes = $tr_classes ? 'class="'.implode(' ', $tr_classes).'"' : ''; $has_index = strpos($location, ':index') !== false; $index_not_supported = $has_index && strpos($location, 'p:') === 0 && strpos($page_visibility, 'singular:') === 0; ob_start(); ?>'.$error.'
'; } echo json_encode(array( 'error' => !empty($error), 'html' => $output, )); } exit; } /* * The options page (form) * * @since 1.0 */ public function settingsPage(){ // check for old ads form the "Ad module" in Atom themes $this->autoImportFromAtom(); ?> getRegisteredAds($properties); if(!$properties) return false; } extract($properties); // check if ad is disabled; if it is we're not going further if(!$active) return false; // start optimistic :D $visible = true; // page visibility if(strpos($page_visibility, 'singular:') === 0) $visible = is_singular(substr($page_visibility, 9)); elseif(strpos($page_visibility, 'tax:') === 0) $visible = is_post_type_archive(substr($page_visibility, 4)); elseif($page_visibility !== 'any') $visible = call_user_func("is_{$page_visibility}"); // user visibility; // no point checking if we're not on the right page if($visible){ if((strpos($user_visibility, 'role:') === 0) && is_user_logged_in()) $visible = in_array(substr($user_visibility, 5), wp_get_current_user()->roles); elseif($user_visibility === 'visitors') $visible = !is_user_logged_in(); elseif($user_visibility === 'no-admin') $visible = !current_use_can('administrator'); } // some ad types will want to do further visibility checks, // like the image ad type (for tracking + max click limit) if($visible) $visible = $this->ad_types[$type]->isVisible($properties); return apply_filters('ad_manager_visibility_check', $visible, $properties); } /* * Get the code associated with an ad. * * @since 1.0 * @param int $id Ad ID * @param string $content Optional, content to append/prepend the code to * @return string */ public function getAdCode($id, $content = ''){ $ad = $this->getRegisteredAds($id); if(!$ad) return $content; extract($ad); if(!isset($this->ad_types[$type])) return $content; $code = $this->ad_types[$type]->getCode($id, $ad); if($content === '') return $code; // handle built-in locations: the_content or comment_text actions; // after content if(strpos($location, 'after_') !== false) return $content.$code; // before content if(strpos($location, 'before_') !== false) return $code.$content; // half-way trough - this could be slow -- @todo $test = ''; // stupid wp still supports php 5.2, so we can't rely on closures :( $content = preg_replace_callback('/(\.|\?|\!)(?!([^<]+)?>)/i', create_function('$matches', 'return $matches[1]."";'), $content); $parts = explode($test, $content); $pos = floor(count($parts) / 2); if(isset($parts[$pos])) $parts[$pos] .= $code; $content = str_replace($test, '', implode($test, $parts)); return $content; } /* * Displays all ads associated with a specific location. * This function should only be called trough an action, because the action tag represents the location * * @since 1.0 * @param string $content Post or comment content, only available for default locations (the_content and comment_text actions) */ public function output($content = ''){ // tracks the current action index, for locations that require it static $current_index = array(); // action tag name, required to indentify ads queued on this action/location $place = current_filter(); $in_content = in_array($place, array('the_content', 'comment_text')); if(!isset($current_index[$place])) $current_index[$place] = 0; // don't go further if there are no ads queued on this action if(!isset($this->queue[$place])) return; // we have ads, display them foreach($this->queue[$place] as $key => $id){ // check if this location requires an action index; // if it does, insert the ad after "index" number of actions have been ran if(isset($this->location_index_conditions[$id])){ // increase index counter for this action $current_index[$place]++; // skip this location, because we don't have an index match if($this->location_index_conditions[$id] !== $current_index[$place]) continue; } // remove from queue, because this filter can run later unset($this->queue[$place][$key]); if($in_content) return $this->getAdCode($id, $content); echo $this->getAdCode($id, $content); } if($in_content) return $content; } /* * Determines which ads are visible in the current context and queues them for display * * @since 1.0 */ public function run(){ foreach($this->getRegisteredAds() as $id => $properties){ extract($properties); if(!$this->isVisible($properties) || $location === 'shortcode') continue; // custom action if($location !== 'action'){ // determine if the location needs an index if(($index_pos = strpos($location, ':index')) !== false){ $location = substr($location, 0, $index_pos); // ignore the index conditions for posts if the page is set to singular-type if(!(strpos($location, 'p:') === 0 && strpos($page_visibility, 'singular:') === 0)) $this->location_index_conditions[$id] = $index; } // drop "theme:" prefix if we have it if(strpos($location, 'theme:') === 0) $action = substr($location, 6); elseif(strpos($location, 'p:') === 0) $action = 'the_content'; elseif(strpos($location, 'c:') === 0) $action = 'comment_text'; } if(!isset($this->queue[$action])) $this->queue[$action] = array(); $this->queue[$action][] = $id; } foreach($this->queue as $place => $code) add_filter($place, array($this, 'output')); } public function autoImportFromAtom(){ if(!class_exists('Atom') || !function_exists('atom')) return; $theme_options = atom()->options(); $imported = array(); if(empty($theme_options['advertisments'])) return; foreach($theme_options['advertisments'] as $ad){ // defaults $type = 'AdHTML'; $active = true; $auto_scan = true; $action = ''; // html code $html = $ad['html']; $index = isset($ad['n']) ? (int)$ad['n'] : 4; // location $location = "atom_{$ad['place']}"; foreach($this->theme_ad_locations as $id => $label) if(strpos($id, $location) !== false) $location = $id; if(!in_array($location, array_keys(call_user_func_array('array_merge', $this->getAdLocations())))) $location = 'shortcode'; // page vis. $page = ($ad['page'] === 'single') ? 'singular:post' : $ad['page']; $page_visibility = in_array($page, array_keys($this->getAdPages())) ? $page : 'any'; // user vis. switch($ad['to']){ case '0': $user_visibility = 'anyone'; break; case '1': $user_visibility = 'visitors'; break; default: $role = "role:{$ad['to']}"; $user_visibility = in_array($role, array_keys($this->getAdUsers())) ? $role : 'anyone'; break; } $imported[] = compact('type', 'active', 'auto_scan', 'location', 'action', 'index', 'page_visibility', 'user_visibility', 'html'); } $existing_ads = $this->getRegisteredAds(); $starting_id = empty($existing_ads) ? 1 : max(array_keys($existing_ads)) + 1; foreach($imported as $ad) $existing_ads[$starting_id++] = $ad; $this->updateRegisteredAds($existing_ads); // remove old ads from the theme unset($theme_options['advertisments']); atom()->setOptions($theme_options); ?>getThemeName()); ?>