Index: includes/alerts.inc.php
===================================================================
--- includes/alerts.inc.php	(revision 7719)
+++ includes/alerts.inc.php	(working copy)
@@ -604,8 +604,9 @@
   // Short circuit this check if the device is either disabled or ignored.
   if ($ignore && ($device['disable'] == 1 || $device['ignore'] == 1)) { return FALSE; }
 
-  $sql   = "SELECT COUNT(*) FROM `devices`";
-  $sql  .= " WHERE `device_id` = ?";
+  $select   = "SELECT COUNT(*) FROM `devices` d";
+  $join = "";
+  $sql  = " WHERE d.`device_id` = ?";
   $param = array($device['device_id']);
 
   foreach ($attributes as $attrib)
@@ -614,24 +615,24 @@
     {
       case 'ge':
       case '>=':
-        $sql .= ' AND `' . $attrib['attrib'] . '` >= ?';
+        $sql .= ' AND d.`' . $attrib['attrib'] . '` >= ?';
         $param[] = $attrib['value'];
         break;
       case 'le':
       case '<=':
-        $sql .= ' AND `' . $attrib['attrib'] . '` <= ?';
+        $sql .= ' AND d.`' . $attrib['attrib'] . '` <= ?';
         $param[] = $attrib['value'];
         break;
       case 'gt':
       case 'greater':
       case '>':
-        $sql .= ' AND `' . $attrib['attrib'] . '` > ?';
+        $sql .= ' AND d.`' . $attrib['attrib'] . '` > ?';
         $param[] = $attrib['value'];
         break;
       case 'lt':
       case 'less':
       case '<':
-        $sql .= ' AND `' . $attrib['attrib'] . '` < ?';
+        $sql .= ' AND d.`' . $attrib['attrib'] . '` < ?';
         $param[] = $attrib['value'];
         break;
       case 'notequals':
@@ -638,7 +639,7 @@
       case 'isnot':
       case 'ne':
       case '!=':
-        $sql .= ' AND `' . $attrib['attrib'] . '` != ?';
+        $sql .= ' AND d.`' . $attrib['attrib'] . '` != ?';
         $param[] = $attrib['value'];
         break;
       case 'equals':
@@ -646,7 +647,7 @@
       case 'is':
       case '==':
       case '=':
-        $sql .= ' AND `' . $attrib['attrib'] . '` = ?';
+        $sql .= ' AND d.`' . $attrib['attrib'] . '` = ?';
         $param[] = $attrib['value'];
         break;
       case 'match':
@@ -653,7 +654,7 @@
       case 'matches':
         $attrib['value'] = str_replace('*', '%', $attrib['value']);
         $attrib['value'] = str_replace('?', '_', $attrib['value']);
-        $sql .= ' AND IFNULL(`' . $attrib['attrib'] . '`, "") LIKE ?';
+        $sql .= ' AND IFNULL(d.`' . $attrib['attrib'] . '`, "") LIKE ?';
         $param[] = $attrib['value'];
         break;
       case 'notmatches':
@@ -661,12 +662,12 @@
       case '!match':
         $attrib['value'] = str_replace('*', '%', $attrib['value']);
         $attrib['value'] = str_replace('?', '_', $attrib['value']);
-        $sql .= ' AND IFNULL(`'. $attrib['attrib'] . '`, "") NOT LIKE ?';
+        $sql .= ' AND IFNULL(d.`'. $attrib['attrib'] . '`, "") NOT LIKE ?';
         $param[] = $attrib['value'];
         break;
       case 'regexp':
       case 'regex':
-        $sql .= ' AND IFNULL(`' . $attrib['attrib'] . '`, "") REGEXP ?';
+        $sql .= ' AND IFNULL(d.`' . $attrib['attrib'] . '`, "") REGEXP ?';
         $param[] = $attrib['value'];
         break;
       case 'notregexp':
@@ -673,30 +674,35 @@
       case 'notregex':
       case '!regexp':
       case '!regex':
-        $sql .= ' AND IFNULL(`' . $attrib['attrib'] . '`, "") NOT REGEXP ?';
+        $sql .= ' AND IFNULL(d.`' . $attrib['attrib'] . '`, "") NOT REGEXP ?';
         $param[] = $attrib['value'];
         break;
       case 'in':
       case 'list':
-        $sql .= generate_query_values(explode(',', $attrib['value']), $attrib['attrib']);
+        $sql .= generate_query_values(explode(',', $attrib['value']), "d.".$attrib['attrib']);
         break;
       case '!in':
       case '!list':
       case 'notin':
       case 'notlist':
-        $sql .= generate_query_values(explode(',', $attrib['value']), $attrib['attrib'], '!=');
+        $sql .= generate_query_values(explode(',', $attrib['value']), "d.".$attrib['attrib'], '!=');
         break;
-      case 'include': // FIXME, what is this?
+      case 'include':
+      case 'includes': // FIXME, what is this?
         switch($attrib['attrib'])
         {
-          case 'group':
-
-          break;
+        case 'group':
+            $join .= " INNER JOIN `group_table` gt on d.`device_id`=gt.`device_id`";
+            $join .= " INNER JOIN `groups` g on g.`group_id`=gt.`group_id`";
+            $sql .= " AND g.`group_name` = ?";
+            $param[] = $attrib['value'];
+            break;
         }
         break;
     }
   }
 
+  $sql = $select . $join . $sql;
   $device_count = dbFetchCell($sql, $param);
 
   if ($device_count == 0)
Index: includes/groups.inc.php
===================================================================
--- includes/groups.inc.php	(revision 7719)
+++ includes/groups.inc.php	(working copy)
@@ -57,6 +57,7 @@
   $msg .= PHP_EOL;
   $msg .= '  <h4 style="">Groups matching this device:</h4> ';
 
+  // loop cached device<->group sets
   foreach ($groups['group'] as $group_id => $group_test)
   {
     $msg .= '<span class="label label-info">'.escape_html($group_test['group_name']).'</span> ';
@@ -101,7 +102,7 @@
         {
           if ($groups_table_db[$entity_type][$entity_id][$group_id]['group_assocs'] != implode($b,",")) { $update_array = array('group_assocs' => implode($b,","));  }
           #echo("[".$groups_table_db[$entity_type][$entity_id][$group_id]['group_assocs']."][".implode($b,",")."]");
-          if (is_array($update_array))
+          if (isset($update_array) && is_array($update_array))
           {
             dbUpdate($update_array, 'group_table', '`group_table_id` = ?', array($groups_table_db[$entity_type][$entity_id][$group_id]['group_table_id']));
             unset($update_array);
@@ -150,26 +151,87 @@
   //if ($device['ignore'] == 1 || $device['disabled'] == 1) { return array(); }
 
   $groups = cache_groups();
+  $recursiveRules = []; // will hold all recursive rules
+  $groups_index = [];   // a zero based index of all applied groups
+                        // as opposed to the group_id based index of groups in $groups_new
+
   foreach ($groups['assoc'] as $assoc_key => $assoc)
   {
+    // Save rule if it's recursive for later checkings
+    $condition = $assoc['device_attribs'][0]['condition'];
+    if (preg_match('#include[s]?#i', $condition)) {
+      $recursiveRules[$assoc_key] = $assoc;
+      continue;
+    }
+
+    // Build group array
     if (match_device($device, $assoc['device_attribs'], FALSE))
     {
       $assoc['group_id'];
       $groups['group'][$assoc['group_id']]['assoc'][$assoc_key] = $groups['assoc'][$assoc_key];
       $groups_new['group'][$assoc['group_id']] = $groups['group'][$assoc['group_id']];
+      $groups_index[] = $groups['group'][$assoc['group_id']];
     } else {
       unset($groups['assoc'][$assoc_key]);
     }
   }
 
+  // Process recursive rules
+  //
+  // This will loop through all already applied groups and those groups
+  // that will be applied through recursive rules to check for new
+  // recursive rules that apply.
+  for ($i=0; $i<count($groups_index); $i++) {
+    $rules = [];
+    $group_name = $groups_index[$i]['group_name'];
+
+    foreach ($recursiveRules as $ruleid => $rec) {
+      // check if rule targets the group
+      $attribs = $rec['device_attribs'][0];
+      if ($attribs['value'] == $group_name) {
+        $rules[$ruleid] = $rec;
+      }
+    }
+
+    // process found rules!
+    if (count($rules) != 0) {
+      foreach($rules as $ruleid => $rule) {
+        $done = false;
+        // check if recursive rule is already applied (and avoid endless loops)
+        foreach($groups_index as $compare) {
+          if ($compare['group_name'] == $groups['group'][$rule['group_id']]['group_name']) {
+            // device is already in that group
+            $done = true;
+            break;
+          }
+        }
+
+        // add new group
+        if (!$done) {
+          $groups['group'][$rule['group_id']]['assoc'][$rule['group_assoc_id']] = $groups['assoc'][$rule['group_assoc_id']];
+          $groups_new['group'][$rule['group_id']] = $groups['group'][$rule['group_id']];
+          $groups_index[] = $groups['group'][$rule['group_id']];
+        }
+      }
+    } // end: rules found and processed
+  }
+
   return $groups_new;
 }
 
-// DOCME needs phpdoc block
+/**
+ * Returns the group id for the group name supplied.
+ *
+ * @param   string  group_name
+ * @return  int     group id
+*/
 // TESTME needs unit testing
 function group_id_by_name($group_name)
 {
+  $qry = "SELECT `group_id` FROM `groups` WHERE `group_name` = ?;";
+  $id = dbFetchCell($qry, [$group_name]);
 
+  return $id;
 }
 
 // DOCME needs phpdoc block
