diff --git a/sites/all/modules/ad/adcache.inc b/sites/all/modules/ad/adcache.inc
index 3701d4f..5aacfb3 100644
--- a/sites/all/modules/ad/adcache.inc
+++ b/sites/all/modules/ad/adcache.inc
@@ -509,15 +509,16 @@ function adserve_cache_display($ids) {
   }
   $output = preg_replace('&/@HOSTID___&', $replace, $output);
 
-  // there was an error, hide the output in comments
+  // there was an error, hide output
   if (adserve_variable('error')) {
-    $output = "<!-- $output -->";
+    _debug_echo("There was an error ($output), producing no output.");
+  }
+  else {
+    // allow custom text to be displayed before and after advertisement
+    $init_text = adserve_invoke_hook('init_text', 'append');
+    $exit_text = adserve_invoke_hook('exit_text', 'append');
+    $output = $init_text . $output . $exit_text;
   }
-
-  // allow custom text to be displayed before and after advertisement
-  $init_text = adserve_invoke_hook('init_text', 'append');
-  $exit_text = adserve_invoke_hook('exit_text', 'append');
-  $output = $init_text . $output . $exit_text;
 
   _debug_memory();
 
@@ -525,6 +526,9 @@ function adserve_cache_display($ids) {
   switch (adserve_variable('ad_display')) {
     case 'javascript':
     default:
+      if (adserve_variable('error')) {
+        exit(0);
+      }
       $output = str_replace(array("\r", "\n", "<", ">", "&"),
                             array('\r', '\n', '\x3c', '\x3e', '\x26'),
                             addslashes($output));
@@ -547,6 +551,9 @@ function adserve_cache_display($ids) {
       exit(0);
     case 'iframe':
     case 'jquery':
+      if (adserve_variable('error')) {
+        exit(0);
+      }
       if (!adserve_variable('debug')) {
         // Tell the web browser not to cache this frame so the ad refreshes
         // each time the page is viewed.
@@ -567,10 +574,12 @@ function adserve_cache_display($ids) {
       print "$output";
       exit(0);
     case 'raw':
+      if (adserve_variable('error')) {
+        return '';
+      }
       _debug_echo('Output: '. htmlentities($output));
       chdir(adserve_variable('ad_dir'));
       return $output;
-
   }
 
   _debug_echo('Output: '. htmlentities($output));
diff --git a/sites/all/modules/ad/channel/ad_channel.inc b/sites/all/modules/ad/channel/ad_channel.inc
index df96cde..d75b25c 100644
--- a/sites/all/modules/ad/channel/ad_channel.inc
+++ b/sites/all/modules/ad/channel/ad_channel.inc
@@ -23,11 +23,40 @@ function ad_channel_cache_filter($ads) {
   $valid_ads = array();
   $nochannel_fallback_ads = array();
   $nochannel_percent = array();
+
+  // preparing node-level data for taxonomy filter
+  // TODO: not exactly too efficient; should re-implement w/ no-DB caching
+  $node_id = adserve_variable('url');
+  $is_node = strpos($node_id, 'node/');
+  if ($is_node !== false) {
+    $node = new stdClass;
+    $node_id = substr($node_id, 5);
+    $node->vid = $node_id;
+    // alternative: use in-loop call to taxonomy_node_get_terms_by_vocabulary($node, $vid, $key = 'tid')
+    $terms = taxonomy_node_get_terms($node);
+    $allowed_vids = variable_get('ad_channel_allowed_vids', array());
+    #echo "allowed vocabulary ids:"; var_dump($allowed_vids);
+    #echo "node 'object': "; var_dump($node);
+    #echo "node terms: "; var_dump($terms);
+    // clean node's $terms from non-matching vocabularies, flatten $terms
+    $terms_new = array();
+    foreach ($terms as $term_id => $tmp_obj) {
+      if (in_array($tmp_obj->vid, $allowed_vids)) {
+        $terms_new[$tmp_obj->vid] = $term_id;
+      }
+    }
+    $terms = $terms_new;
+    unset($terms_new);
+    unset($tmp_obj);
+    unset($term_id);
+    //echo "terms after cleaning/flattening: "; print_r($terms);
+  }
+
   // determine which channels each advertisement is assigned to
   foreach ($ads as $aid) {
     _debug_echo("ad_channel_cache: checking aid($aid)");
-    if (is_array($channels['ads']) && isset($channels['ads'][$aid]) &&
-        is_array($channels['ads'][$aid])) {
+    if (is_array($channels['ads']) && isset($channels['ads'][$aid]) && is_array($channels['ads'][$aid])) {
+      reset($channels['ads'][$aid]);
       foreach ($channels['ads'][$aid] as $chid) {
         $channel = $channels['channels'][$chid];
         $display_by_url = $channel->display;
@@ -71,6 +100,47 @@ function ad_channel_cache_filter($ads) {
           _debug_echo("ad_channel_cache: aid($aid) is not valid");
         }
       }
+
+      // ads by taxonomy filter
+
+      // skip taxonomy filter if: $match is true, this ad is already valid,
+      // or if not on a node page (taxonomy makes sense only for nodes)
+      if ($match || $is_node === false || (is_array($valid_ads) && is_array($valid_ads[$chid]) && in_array($aid, $valid_ads[$chid]))) {
+        continue;
+      }
+      reset($channels['ads'][$aid]);
+      foreach ($channels['ads'][$aid] as $chid) {
+        $channel = $channels['channels'][$chid];
+        $channel_terms = variable_get("ad_channel_allowed_terms_$chid", 0);
+        $match = false;
+        if (is_array($channel_terms)) {
+          foreach ($terms as $vid => $term_id) {
+            foreach ($channel_terms as $vid_ch => $term_ch) {
+              if ($vid == $vid_ch && array_key_exists($term_id, $term_ch)) {
+                $match = true;
+                break 2;
+              }
+            }
+          }
+        }
+        _debug_echo("ad_channel_cache: checking aid($aid) against channel($chid) url (" . adserve_variable('url') . ") terms (" . serialize($channel_terms) . ") node terms (" . serialize($terms) . ") match($match)");
+        //echo "ad_channel_cache: checking aid($aid) against channel($chid) url (" . adserve_variable('url') . ") terms (" . serialize($channel_terms) . ") node terms (" . serialize($terms) . ") match($match)\n";
+        // display ad on matching taxonomy terms
+        if ($match) {
+          _debug_echo("ad_channel_cache: aid($aid) is valid (taxonomy term matched)");
+          $valid_ads[$chid][] = $aid;
+          if ($nochannel_display == 1) {
+            $nochannel_percent[$chid] = $channel->no_channel_percent;
+            _debug_echo("ad_channel_cache: channel($chid) no_channel_percent($nochannel_percent[$chid])");
+          }
+          else {
+            _debug_echo("ad_channel_cache: channel($chid)");
+          }
+          break;
+        }
+        // no match so we didn't hit a break, ad is not valid
+        _debug_echo("ad_channel_cache: aid($aid) is not valid (taxonomy terms didn't match)");
+      }
     }
     else {
       // no channel information for ad
diff --git a/sites/all/modules/ad/channel/ad_channel.module b/sites/all/modules/ad/channel/ad_channel.module
index 96ad8e1..e975f76 100644
--- a/sites/all/modules/ad/channel/ad_channel.module
+++ b/sites/all/modules/ad/channel/ad_channel.module
@@ -636,6 +636,32 @@ function ad_channel_admin_settings() {
     '#description' => t('Optionally limit the number of channels that a single advertisement can be assigned to.'),
   );
 
+  // Allowed taxonomy vocabularies
+  $vocs = taxonomy_get_vocabularies();
+  unset($vocs[_ad_get_vid()]); // hide ad groups vocabulary
+  // preparing associative array for select
+  $vocabularies = array();
+  foreach ($vocs as $vid => $vocabulary) {
+   $vocabularies[$vid] = t($vocabulary->name);
+  }
+
+  $selected_vocs = variable_get('ad_channel_allowed_vids', NULL);
+  $collapsed = is_array($selected_vocs) && !empty($selected_vocs) ? FALSE : TRUE;
+  $form['vocabulary_rules'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Allowed vocabularies for use in channels'),
+    '#collapsible' => TRUE,
+    '#collapsed' => $collapsed
+  );
+  $form['vocabulary_rules']['ad_channel_allowed_vids'] = array(
+    '#type' => 'select',
+    '#title' => t('Ctrl-click vocabularies you want to use for ad channel filtering'),
+    '#default_value' => $selected_vocs,
+    '#options' => $vocabularies,
+    '#size' => min(6, sizeof($vocabularies)),
+    '#multiple' => true
+  );
+
   return system_settings_form($form);
 }
 
@@ -1125,6 +1151,34 @@ function ad_channel_admin_channel($form_state, $chid = 0) {
     '#default_value' => $chid ? unserialize($channel->urls) : '',
   );
 
+  // taxonomy rules: display ads only for nodes with matching taxonomy term(s)
+  $form['taxonomy_rules'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Taxonomy rules'),
+    '#description' => t('Display advertisements from this channel on the nodes with specific taxonomy terms'),
+    '#collapsible' => TRUE,
+    '#collasped' => FALSE
+  );
+
+  // 1. Get allowed vocabularies
+  $vocabularies = variable_get('ad_channel_allowed_vids', NULL);
+  // 2. iterate all allowed vocabularies, displaying select for each one
+  $form['taxonomy_rules']['allowed_terms']['#tree'] = true;
+  foreach ($vocabularies as $vid) {
+    $vocabulary = taxonomy_vocabulary_load($vid);
+    $help = ($help) ? $help : filter_xss_admin($vocabulary->help);
+    if (!$vocabulary->multiple) {
+        $blank = ($vocabulary->required) ? t('- Please choose -') : t('- None selected -');
+    }
+    else {
+        $blank = ($vocabulary->required) ? 0 : t('- None -');
+    }
+    // assuming $value is for selected terms
+    $selected = variable_get('ad_channel_allowed_terms_' . $chid, NULL);
+    $selected = $selected[$vid];
+    $form['taxonomy_rules']['allowed_terms'][$vid] = _taxonomy_term_select(check_plain($vocabulary->name), $name, $selected, $vid, $help, intval($vocabulary->multiple), $blank);
+  }
+
   // Group rules
   $groups = taxonomy_get_tree(_ad_get_vid());
   $collapsed = is_array($groups) && !empty($groups) ? FALSE : TRUE;
@@ -1237,6 +1291,9 @@ function ad_channel_admin_channel_submit($form, &$form_state) {
   switch ($form_state['values']['op']) {
     case t('Create'):
       db_query("INSERT INTO {ad_channel} (name, description, conid, weight, display, no_channel_percent, urls, groups, inventory) VALUES('%s', '%s', %d, %d, %d, '%s', '%s', '%s', %d)", $form_state['values']['name'], $form_state['values']['description'], $form_state['values']['conid'], $form_state['values']['weight'], $form_state['values']['display'], $form_state['values']['no_channel_percent'], serialize($urls), serialize($form_state['values']['groups']), $form_state['values']['inventory']);
+      if (isset($form_state['values']['allowed_terms']) && is_array($form_state['values']['allowed_terms'])) {
+        variable_set('ad_channel_allowed_terms_' . db_last_insert_id('ad_channel', 'chid'), $form_state['values']['allowed_terms']);
+      }
       drupal_set_message(t('The %name channel has been created.', array('%name' => $form_state['values']['name'])));
       break;
     case t('Update'):
@@ -1251,6 +1308,9 @@ function ad_channel_admin_channel_submit($form, &$form_state) {
         }
       }
       db_query("UPDATE {ad_channel} SET name = '%s', description = '%s', conid = %d, weight = %d, display = %d, urls = '%s', groups = '%s', no_channel_percent = %d, inventory = %d WHERE chid = %d", $form_state['values']['name'], $form_state['values']['description'], $form_state['values']['conid'], $form_state['values']['weight'], $form_state['values']['display'], serialize($urls), serialize($groups), $form_state['values']['no_channel_percent'], $form_state['values']['inventory'], $form_state['values']['chid']);
+      if (isset($form_state['values']['allowed_terms']) && is_array($form_state['values']['allowed_terms'])) {
+        variable_set('ad_channel_allowed_terms_' . $form_state['values']['chid'], $form_state['values']['allowed_terms']);
+      }
       drupal_set_message(t('The %name channel has been updated.', array('%name' => $form_state['values']['name'])));
       break;
     case t('Delete'):
@@ -1286,6 +1346,7 @@ function ad_channel_admin_confirm_delete_channel_submit($form, &$form_state) {
   $channel = _ad_channel_get_channels($form_state['values']['chid']);
   if ($channel->chid) {
     db_query('DELETE FROM {ad_channel} WHERE chid = %d', $channel->chid);
+    variable_del('ad_channel_allowed_terms_' . $channel->chid);
     drupal_set_message(t('The %name channel has been deleted.', array('%name' => $channel->name)));
   }
   drupal_goto('admin/content/ad/channel');
