You are here

og.module in Organic groups 5.8

File

og.module
View source
<?php

// Selective groups states. chosen by the group admin
define('OG_OPEN', 0);
define('OG_MODERATED', 1);
define('OG_INVITE_ONLY', 2);
define('OG_CLOSED', 3);

// site admin chooses in og_settings() whether group creator can put his group on the registration form
define('OG_REGISTRATION_NEVER', 0);
define('OG_REGISTRATION_ALWAYS', 1);
define('OG_REGISTRATION_CHOOSE_TRUE', 2);
define('OG_REGISTRATION_CHOOSE_FALSE', 3);

// site admin chooses in og_settings() whether group creator can put his group in the Groups directory
define('OG_DIRECTORY_NEVER', 0);
define('OG_DIRECTORY_ALWAYS', 1);
define('OG_DIRECTORY_CHOOSE_TRUE', 2);
define('OG_DIRECTORY_CHOOSE_FALSE', 3);
function og_help($section) {
  switch ($section) {
    case !empty($section) && strstr($section, 'admin/build/block/configure/og'):
      return t('Group specific blocks are only visible on group pages and not on systemwide pages like the home page or admin pages.');
    case 'admin/settings/og':
      return t('In order to let group admins determine their own group theme, you must enable multiple themes using !page.', array(
        '!page' => l(t('theme configuration page'), 'admin/build/themes'),
      ));
  }
}
function og_menu($may_cache) {
  global $user;
  $items = array();
  $access = $user->uid;

  // login is required
  if ($may_cache) {

    // anon users should be able to get to the join page
    $items[] = array(
      'path' => 'og/subscribe',
      'type' => MENU_CALLBACK,
      'callback' => 'og_menu_check_node_view',
      'access' => TRUE,
      'callback arguments' => array(
        'og_subscribe',
      ),
      'title' => t('Join group'),
    );
    $items[] = array(
      'path' => 'og/opml',
      'type' => MENU_CALLBACK,
      'callback' => 'og_opml',
      'access' => $access,
      'title' => t('OPML'),
    );
    $items[] = array(
      'path' => 'og/unsubscribe',
      'type' => MENU_CALLBACK,
      'callback' => 'og_menu_check_node_view',
      'callback arguments' => array(
        'og_unsubscribe',
      ),
      'access' => $access,
      'title' => t('Leave group'),
    );
    $items[] = array(
      'path' => 'og/approve',
      'type' => MENU_CALLBACK,
      'callback' => 'og_approve',
      'access' => $access,
      'title' => t('Approve membership request'),
    );
    $items[] = array(
      'path' => 'og/deny',
      'type' => MENU_CALLBACK,
      'callback' => 'og_deny',
      'access' => $access,
      'title' => t('Deny membership request'),
    );
    $items[] = array(
      'path' => 'og/create_admin',
      'type' => MENU_CALLBACK,
      'callback' => 'og_create_admin',
      'access' => $access,
      'title' => t('Create group administrator'),
    );
    $items[] = array(
      'path' => 'og/delete_admin',
      'type' => MENU_CALLBACK,
      'callback' => 'og_delete_admin',
      'access' => $access,
      'title' => t('Delete group administrator'),
    );
    $items[] = array(
      'path' => 'og/feed',
      'callback' => 'og_feed',
      'title' => t('Group feed'),
      'type' => MENU_CALLBACK,
      'access' => user_access('access content'),
    );

    // members only
    $items[] = array(
      'path' => "og/invite",
      'callback' => 'og_menu_check',
      'title' => t('Send invitation'),
      'callback arguments' => array(
        'og_invite_page',
      ),
      'type' => MENU_CALLBACK,
      'access' => $access,
    );
    $items[] = array(
      'path' => "og/manage",
      'callback' => 'og_menu_check',
      'title' => t('Manage membership'),
      'callback arguments' => array(
        'og_manage',
      ),
      'type' => MENU_CALLBACK,
      'access' => $access,
    );
    $items[] = array(
      'path' => 'og/activity',
      'title' => t('Group activity'),
      'callback' => 'og_page_activity',
      'access' => user_access('administer organic groups'),
      'weight' => 4,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/og',
      'title' => t('Organic groups'),
      'description' => t('Administer the suite of organic groups modules.'),
      'position' => 'right',
      'weight' => -5,
      'callback' => 'system_admin_menu_block_page',
      'access' => user_access('administer site configuration'),
    );
    $items[] = array(
      'path' => 'admin/og/og',
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'og_admin_settings',
      ),
      'title' => t('Organic groups configuration'),
      'description' => t('Configure the main Organic groups module (og)'),
      'weight' => -5,
    );
  }
  else {
    drupal_add_css(drupal_get_path('module', 'og') . '/og.css');

    // we get a NOTICE if doing this from within og_theme() so I do it here for now.
    if ($group_node = og_get_group_context()) {
      if ($user->uid || variable_get('cache', CACHE_DISABLED) == CACHE_DISABLED) {
        $_SESSION['og_last'] = $group_node->nid;
      }
    }

    //membership page and its 'add members' tab
    $gid = arg(2);
    if (arg(0) == 'og' && arg(1) == 'users' && is_numeric($gid)) {
      $items[] = array(
        'path' => "og/users/{$gid}",
        'callback' => 'og_menu_check',
        'title' => t('Members'),
        'callback arguments' => array(
          'og_list_users_page',
          $gid,
        ),
        'type' => MENU_CALLBACK,
        'access' => $access,
      );
      $items[] = array(
        'path' => "og/users/{$gid}/list",
        'title' => t('List'),
        'type' => MENU_DEFAULT_LOCAL_TASK,
      );
      if (og_is_picture()) {
        $items[] = array(
          'path' => "og/users/{$gid}/faces",
          'title' => t('Faces'),
          'callback' => 'og_menu_check',
          'callback arguments' => array(
            'og_list_users_faces_page',
            $gid,
          ),
          'type' => MENU_LOCAL_TASK,
        );
      }

      // group admin only
      $node = node_load($gid);
      $items[] = array(
        'path' => "og/users/{$gid}/add_user",
        'callback' => 'drupal_get_form',
        'title' => t('Add members'),
        'callback arguments' => array(
          'og_add_users',
          $gid,
        ),
        'type' => MENU_LOCAL_TASK,
        'weight' => 5,
        'access' => og_is_node_admin($node),
      );
    }

    // Broadcast tab on group node.
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      if (og_is_group_type($node->type)) {
        $items[] = array(
          'path' => 'node/' . arg(1) . '/broadcast',
          'title' => t('Broadcast'),
          'callback' => 'drupal_get_form',
          'callback arguments' => array(
            'og_broadcast_form',
            arg(1),
          ),
          'access' => og_broadcast_access($node),
          'type' => MENU_LOCAL_TASK,
          'weight' => 7,
        );
      }
    }
  }
  return $items;
}

// check for membership and then pass along to the real menu callback
// arguments must be in this order: function, $gid, <extra params required by function>
// we used to register these menu items dynamically for each joined group but non members were falling through and getting /og callback
function og_menu_check() {
  global $user;
  $args = func_get_args();
  $function = array_shift($args);
  $groups = array_keys($user->og_groups);
  if (user_access('administer nodes') || in_array($args[0], $groups)) {
    return call_user_func_array($function, $args);
  }
  else {
    drupal_access_denied();
  }
}

// check for node access 'view' and then pass along to the real menu callback
// arguments must be in this order: function, $gid, <extra params required by function>
function og_menu_check_node_view() {
  global $user;
  $args = func_get_args();
  $function = array_shift($args);
  $gid = $args[0];
  $node = node_load((int) $gid);
  if (node_access('view', $node)) {
    return call_user_func_array($function, $args);
  }
  else {
    drupal_access_denied();
  }
}

/**
 * This processing cannot happen later in the request because
 * - menu items will be in default language if we wait until og_menu(!$may_cache). bad for locale feature
 */
function og_init() {

  // only bother when we are not serving a cached page. check for which function that only exists afterwards
  if (function_exists('drupal_set_content')) {

    // we have to perform a load in order to assure that the $user->og_groups bits are present.
    global $user;
    if ($user->uid) {
      $user = user_load(array(
        'uid' => $user->uid,
      ));
    }
    else {
      $user->og_groups = array();
    }
    require dirname(__FILE__) . '/og_views.inc';
    if (module_exists('workflow_ng')) {
      require dirname(__FILE__) . '/og_workflow_ng.inc';
    }
    og_theme();
    og_set_locale();
  }
}

/**
 * Like locale_initialize(), but includes a check for group language and sets accordingly.
 * Priority goes: user => group => site default
 **/
function og_set_locale() {
  global $user, $locale;
  if (function_exists('locale')) {
    $languages = locale_supported_languages();
    $languages = $languages['name'];
  }
  else {

    // Ensure the locale/language is correctly returned, even without locale.module.
    // Useful for e.g. XML/HTML 'lang' attributes.
    $languages = array(
      'en' => 'English',
    );
  }
  if ($user->uid && isset($languages[$user->language])) {

    // do nothing. user specified language has priority
  }
  elseif ($group_node = og_get_group_context()) {
    if ($group_node->og_language && isset($languages[$group_node->og_language])) {
      $locale = $group_node->og_language;
    }
  }
}

/**
 * Implementation of hook_perm().
 */
function og_perm() {
  return array(
    'administer organic groups',
  );
}

/**
 * Override theme based on what group is being displayed (if any).
 * Be smart about selecting the 'active' group for ambigous urls like node/$nid
 *
 * @param
 *   none
 * @return
 *   none
 */
function og_theme() {
  global $custom_theme;
  $group_node = NULL;

  // a node object containing the 'active' group for this request
  if (arg(0) == 'og' && is_numeric(arg(2))) {
    $group_node = og_set_theme(arg(2));
  }
  elseif (arg(0) == 'node' && is_numeric(arg(1))) {
    $group_node = og_set_theme(arg(1));
  }
  elseif (arg(0) == 'node' && arg(1) == 'add' && arg(2) == 'book' && arg(3) == 'parent') {
    $group_node = og_set_theme(arg(4));
    $_REQUEST['edit']['og_groups'][] = $group_node->nid;

    // checks the right box on node form
  }
  elseif (arg(0) == 'node' && arg(1) == 'add' && isset($_REQUEST['gids'])) {
    $gid = intval(current($_REQUEST['gids']));
    $group_node = node_load($gid);
    $custom_theme = $group_node->og_theme;
  }
  elseif (arg(0) == 'comment' && in_array(arg(1), array(
    'edit',
    'delete',
  )) && is_numeric(arg(2))) {
    $nid = db_result(db_query('SELECT nid FROM {comments} WHERE cid = %d', arg(2)));
    $group_node = og_set_theme($nid);
  }
  elseif (arg(0) == 'comment' && is_numeric(arg(2))) {

    // comment/reply/cid/nid URL pattern.
    $group_node = og_set_theme(arg(2));
  }
  og_set_group_context($group_node);
}

/**
 * API function for getting the group context (if any) for the current request. Used
 * for things like setting current theme and breadcrumbs. This context is set during og_theme()
 *
 * @return $node object
 */
function og_get_group_context() {
  return og_set_group_context();
}
function og_set_group_context($group_node = NULL) {
  static $stored_group_node;
  if (og_is_group_type($group_node->type)) {
    $stored_group_node = $group_node;
  }
  return $stored_group_node;
}
function og_set_theme($nid) {
  global $custom_theme, $user;
  $node = node_load(intval($nid));
  if (og_is_group_type($node->type)) {
    $custom_theme = $node->og_theme;
    return $node;
  }
  else {
    switch (count($node->og_groups)) {
      case 0:
        return NULL;
      case 1:
        $group_node = node_load($node->og_groups[0]);
        $custom_theme = $group_node->og_theme;
        break;
      default:

        // node is in multiple groups. preference goes to the group we showed on the prior page view (if any),
        // then to a group the current user is a member of
        if (in_array($_SESSION['og_last'], $node->og_groups)) {
          $group_node = node_load($_SESSION['og_last']);
          $custom_theme = $group_node->og_theme;
        }
        else {
          $groups = array();

          // intersect the node's groups with the user's groups
          if ($user->uid) {
            $groups = array_intersect($node->og_groups, array_keys($user->og_groups));
          }

          // no user is logged in, or none of the node's groups are the user's groups
          if (empty($groups)) {
            $groups = $node->og_groups;
          }

          // use array_shift and not [0] because array_intersect preserves keys
          $group_node = node_load(array_shift($groups));
          $custom_theme = $group_node->og_theme;
        }
    }
    return $group_node;
  }
}

/**
 * Admins may broadcast messages to all their members.
 *
 * @param $gid
 *   the nid of a group.
 */
function og_broadcast_form($gid) {
  $node = node_load($gid);
  drupal_set_title(t('Send message to %group', array(
    '%group' => $node->title,
  )));
  $result = db_query(og_list_users_sql(1), $gid);
  $txt = format_plural(db_num_rows($result), 'the sole member', 'all @count members');
  if (!$_POST) {
    drupal_set_message(t('Your message will be sent to !count in this group.', array(
      '!count' => l($txt, "og/users/{$gid}"),
    )));
  }
  $form['subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Subject'),
    '#size' => 70,
    '#maxlength' => 250,
    '#description' => t("Enter a subject for your message."),
    '#required' => true,
  );
  $form['body'] = array(
    '#type' => 'textarea',
    '#title' => t('Body'),
    '#rows' => 5,
    '#cols' => 90,
    '#description' => t('Enter the body of your message.'),
    '#required' => true,
  );
  $form['send'] = array(
    '#type' => 'submit',
    '#value' => t('Send message'),
  );
  $form['gid'] = array(
    '#type' => 'value',
    '#value' => $gid,
  );
  return $form;
}
function og_broadcast_form_submit($form_id, $form_values) {
  global $user;
  $sql = og_list_users_sql(1);
  $result = db_query($sql, $form_values['gid']);
  $recipients = array();
  while ($row = db_fetch_object($result)) {
    $recipients[] = $row->uid;
  }
  $node = node_load($form_values[gid]);
  $variables = array(
    '@group' => $node->title,
    '@body' => $form_values['body'],
    '@site' => variable_get('site_name', drupal),
    '!url_group' => url("node/{$node->nid}", NULL, NULL, TRUE),
    '!url_unsubscribe' => url("og/unsubscribe/{$node->nid}", NULL, NULL, TRUE),
  );
  $message = array(
    'from' => $user,
    'subject' => $form_values['subject'],
    'body' => _og_user_mail_text('og_admin_email_body', $variables),
  );

  // Send notifications to each member; Sending an array of recipients implies
  // that this is a bulk message.
  module_invoke_all('og', 'user broadcast', $gid, $recipients, $message);
  drupal_set_message(format_plural(count($recipients), '1 message queued for delivery.', '@count messages queued for delivery.'));
  drupal_goto("node/{$form_values[gid]}");
}
function og_manage($gid) {
  global $user;
  $group = node_load($gid);
  $bc[] = array(
    'path' => "og",
    'title' => t('Groups'),
  );
  $bc[] = array(
    'path' => "node/{$gid}",
    'title' => $group->title,
  );
  menu_set_location($bc);
  return drupal_get_form('og_manage_form', $group);
}
function og_manage_form($group) {
  global $user;

  // avoid double messages on POST
  if (!$_POST) {

    // group manager can't leave
    if ($group->og_selective == OG_CLOSED) {
      drupal_set_message(t('You may not leave this group because it is a %closed group. You should request removal from a group administrator.', array(
        '%closed' => t('closed'),
      )));
    }
    elseif ($group->uid == $user->uid) {
      drupal_set_message(t('You may not leave this group because you are its owner. A site administrator can assign ownership to another user and then you may leave.'));
    }
    else {
      $links[] = l(t('Leave this group'), "og/unsubscribe/{$group->nid}");
      $form['unsubscribe'] = array(
        '#type' => 'markup',
        '#value' => theme('item_list', $links),
      );
    }
  }
  $form['gid'] = array(
    '#type' => 'value',
    '#value' => $group->nid,
  );
  return $form;
}
function og_manage_form_submit($form_id, $form_values) {
  global $user;
  $passed_values = $form_values;
  unset($passed_values['gid'], $passed_values['op'], $passed_values['form_id'], $passed_values['form_token']);
  og_save_subscription($form_values['gid'], $user->uid, $passed_values);
  drupal_set_message(t('Membership saved.'));
}

/**
* Low level function for managing membership
*
* @param $gid node ID of a group
* @param $uid user ID of user
* @param $args an array with details of this membership. Possible array keys are:
    is_active, is_admin, created
*/
function og_save_subscription($gid, $uid, $args = array()) {
  $sql = "SELECT COUNT(*) FROM {og_uid} WHERE nid = %d AND uid = %d";
  $cnt = db_result(db_query($sql, $gid, $uid));
  $time = time();

  // Allow other modules to pass their data inside the $args.
  $og_allowed_args = array(
    'is_active',
    'is_admin',
    'created',
  );
  $return_args = $args;
  foreach ($args as $key => $value) {
    if (!in_array($key, $og_allowed_args)) {
      unset($args[$key]);
    }
  }
  if ($cnt == 0) {

    // this pattern borrowed from user_save()
    $fields = array(
      'nid',
      'uid',
      'created',
      'changed',
    );
    $values = array(
      $gid,
      $uid,
      $args['created'] ? $args['created'] : $time,
      $time,
    );
    unset($args['created']);
    foreach ($args as $key => $value) {
      $fields[] = db_escape_string($key);
      $values[] = $value;
      $s[] = "'%s'";
    }
    db_query('INSERT INTO {og_uid} (' . implode(', ', $fields) . ') VALUES (%d, %d, %d, %d, ' . implode(', ', $s) . ')', $values);
    module_invoke_all('og', 'user insert', $gid, $uid, $return_args);
  }
  else {
    $cond[] = 'changed = ' . $time;
    foreach ($args as $key => $value) {
      $cond[] = db_escape_string($key) . " = '" . db_escape_string($value) . "'";
    }
    $cond = implode(', ', $cond);
    db_query("UPDATE {og_uid} SET {$cond} WHERE nid = %d AND uid = %d", $gid, $uid);
    module_invoke_all('og', 'user update', $gid, $uid, $return_args);
  }
}
function og_delete_subscription($gid, $uid, $args = array()) {
  $sql = "DELETE FROM {og_uid} WHERE nid = %d AND uid = %d";
  db_query($sql, $gid, $uid);

  // Allow other modules to pass their data inside the $args.
  module_invoke_all('og', 'user delete', $gid, $uid, $args);
}
function og_approve($gid, $uid) {
  $node = node_load($gid);
  if (og_is_node_admin($node)) {
    $account = user_load(array(
      'uid' => (int) $uid,
    ));
    if ($account === FALSE) {
      drupal_set_message(t("User approval failed, user no longer available on website."), 'error');
      return '';
    }
    if (in_array($gid, array_keys($account->og_groups))) {
      drupal_set_message(t("!name already approved to group %group.", array(
        '!name' => theme('username', $account),
        '%group' => $node->title,
      )), 'error');
      return '';
    }
    else {
      og_save_subscription($gid, $uid, array(
        'is_active' => 1,
      ));
      drupal_set_message(t('Membership request approved.'));
      $variables = array(
        '@title' => $node->title,
        '!group_url' => url("node/{$node->nid}", NULL, NULL, TRUE),
      );
      $message = array(
        'subject' => _og_user_mail_text('og_approve_user_subject', $variables),
        'body' => _og_user_mail_text('og_approve_user_body', $variables),
      );
      module_invoke_all('og', 'user approve', $gid, $uid, $message);
      drupal_goto("node/{$gid}");
    }
  }
  else {
    drupal_access_denied();
  }
}
function og_deny($gid, $uid) {
  $node = node_load($gid);
  if (og_is_node_admin($node)) {
    og_delete_subscription($gid, $uid);
    drupal_set_message(t('Membership request denied.'));
    $variables = array(
      '@title' => $node->title,
      '!group_url' => url("node/{$node->nid}", NULL, NULL, TRUE),
    );
    $message = array(
      'subject' => _og_user_mail_text('og_deny_user_subject', $variables),
      'body' => _og_user_mail_text('og_deny_user_body', $variables),
    );
    module_invoke_all('og', 'user deny', $gid, $uid, $message);
    drupal_goto("node/{$gid}");
  }
  else {
    drupal_access_denied();
  }
}
function og_create_admin($gid, $uid) {
  $node = node_load($gid);
  if (og_is_node_admin($node)) {
    $account = user_load(array(
      'uid' => $uid,
    ));
    return drupal_get_form('og_create_admin_confirm', $gid, $node, $account);
  }
  else {
    drupal_access_denied();
  }
}

/**
 * OG create admin form
 */
function og_create_admin_confirm($gid, $node, $account) {
  $form['node'] = array(
    '#type' => 'value',
    '#value' => $node,
  );
  $form['account'] = array(
    '#type' => 'value',
    '#value' => $account,
  );
  return confirm_form($form, t('Are you sure you want to make %name a group administrator for the group %title?', array(
    '%name' => $account->name,
    '%title' => $node->title,
  )), "og/users/{$gid}", ' ', t('Confirm'), t('Cancel'));
}

/**
 * Confirm og create admin form
 */
function og_create_admin_confirm_submit($form_id, $form_values) {
  $account = $form_values['account'];
  $node = $form_values['node'];
  og_save_subscription($node->nid, $account->uid, array(
    'is_admin' => 1,
  ));
  drupal_set_message(t('%name was promoted to %ga.', array(
    '%name' => $account->name,
    '%ga' => t('group administrator'),
  )));
  $variables = array(
    '@group' => $node->title,
    '!group_url' => url("node/{$node->nid}", NULL, NULL, TRUE),
    '@username' => $account->name,
  );
  $message = array(
    'subject' => _og_user_mail_text('og_new_admin_subject', $variables),
    'body' => _og_user_mail_text('og_new_admin_body', $variables),
  );
  module_invoke_all('og', 'admin create', $node->nid, $account->uid, $message);
  return "og/users/{$node->nid}";
}
function og_delete_admin($gid, $uid) {
  $node = node_load($gid);
  if (og_is_node_admin($node)) {
    $account = user_load(array(
      'uid' => $uid,
    ));
    return drupal_get_form('og_remove_admin_confirm', $gid, $node, $account);
  }
  else {
    drupal_access_denied();
  }
}

/**
 * OG remove admin form
 */
function og_remove_admin_confirm($gid, $node, $account) {
  $form['gid'] = array(
    '#type' => 'value',
    '#value' => $gid,
  );
  $form['account'] = array(
    '#type' => 'value',
    '#value' => $account,
  );
  return confirm_form($form, t('Are you sure you want to remove %name as a group administrator for the group %title?', array(
    '%name' => $account->name,
    '%title' => $node->title,
  )), "og/users/{$gid}", ' ', t('Remove'), t('Cancel'));
}

/**
 * Confirm og remove admin form
 */
function og_remove_admin_confirm_submit($form_id, $form_values) {
  $account = $form_values['account'];
  $gid = $form_values['gid'];
  og_save_subscription($gid, $account->uid, array(
    'is_admin' => 0,
  ));
  drupal_set_message(t('%name is no longer a %ga.', array(
    '%name' => $account->name,
    '%ga' => t('group administrator'),
  )));
  return "og/users/{$gid}";
}
function og_invite_page($gid) {
  $node = node_load($gid);
  if ($node->og_selective < OG_INVITE_ONLY || og_is_node_admin($node)) {
    return drupal_get_form('og_invite_form', $gid);
  }
  else {
    drupal_access_denied();
  }
}
function og_invite_form($gid) {
  $max = variable_get('og_email_max', 10);
  $form['mails'] = array(
    '#type' => 'textarea',
    '#title' => t('Email addresses or usernames'),
    '#description' => t('Enter up to %max email addresses or usernames. Separate multiple addresses by commas or new lines. Each person will receive an invitation message from you.', array(
      '%max' => $max,
    )),
  );
  $form['pmessage'] = array(
    '#type' => 'textarea',
    '#title' => t('Personal message'),
    '#description' => t('Optional. Enter a message which will become part of the invitation email.'),
  );
  $form['op'] = array(
    '#type' => 'submit',
    '#value' => t('Send invitation'),
  );
  $form['gid'] = array(
    '#type' => 'value',
    '#value' => $gid,
  );
  $form['valid_emails'] = array(
    '#type' => 'value',
    '#value' => array(),
  );
  return $form;
}
function og_invite_form_validate($form_id, $form_values, $form) {
  global $user;
  $max = variable_get('og_email_max', 10);
  $mails = $form_values['mails'];
  $mails = str_replace("\n", ',', $mails);
  $emails = explode(',', $mails);
  if (count($emails) > $max) {
    form_set_error('mails', t('You may not specify more than %max email addresses or usernames.', array(
      '%max' => $max,
    )));
  }
  elseif (in_array($user->mail, $emails)) {
    form_set_error('mails', t('You may not invite yourself - @self', array(
      '@self' => $user->mail,
    )));
  }
  else {
    $valid_emails = array();
    $bad = array();
    foreach ($emails as $email) {
      $email = trim($email);
      if (empty($email)) {
        continue;
      }
      if (valid_email_address($email)) {
        $valid_emails[] = $email;
      }
      else {
        $account = user_load(array(
          'name' => check_plain($email),
        ));
        if ($account->mail) {
          $valid_emails[] = $account->mail;
        }
        else {
          $bad[] = $email;
        }
      }
    }
    if (count($bad)) {
      form_set_error('mails', t('Invalid email address or username:') . ' ' . implode(' ', $bad));
    }
    else {

      // Store valid e-mails so we don't have to go through that looping again on submit
      form_set_value($form['valid_emails'], $valid_emails);
    }
  }
}
function og_invite_form_submit($form_id, $form_values) {
  $emails = $form_values['valid_emails'];
  $node = node_load($form_values['gid']);
  $variables = array(
    '@group' => $node->title,
    '@description' => $node->og_description,
    '@site' => variable_get('site_name', 'drupal'),
    '!group_url' => url("og/subscribe/{$node->nid}", NULL, NULL, TRUE),
    '@body' => $form_values['pmessage'],
  );
  global $user;
  $from = $user->mail;
  foreach ($emails as $mail) {
    drupal_mail('og_invite_form', $mail, _og_user_mail_text('og_invite_user_subject', $variables), _og_user_mail_text('og_invite_user_body', $variables), $from);
  }
  drupal_set_message(format_plural(count($emails), '1 invitation sent.', '@count invitations sent.'));
}
function og_subscribe($gid, $uid = NULL) {
  global $user;
  if (is_null($uid)) {
    if ($user->uid) {
      $account = $user;
    }
    else {
      drupal_set_message(t('In order to join this group, you must login or register a new account. After you have successfully done so, you will need to request membership again.'));
      drupal_goto('user');
    }
  }
  else {
    $account = user_load(array(
      'uid' => $uid,
    ));
  }
  $node = node_load($gid);
  if ($node->og_selective >= OG_INVITE_ONLY || $node->status == 0 || !og_is_group_type($node->type)) {
    drupal_access_denied();
    exit;
  }

  // only admins can add another member
  if ($account->uid != $user->uid && !og_is_node_admin($node)) {
    drupal_access_denied();
    exit;
  }
  else {
    if (isset($account->og_groups[$node->nid])) {
      drupal_set_message(t('@user is already a member the group @group', array(
        '@user' => $account->name,
        '@group' => $node->title,
      )));
      drupal_goto('node/' . $node->nid);
    }
    else {
      return drupal_get_form('og_confirm_subscribe', $gid, $node, $account);
    }
  }
}

/**
 * Confirm og membership form
 */
function og_confirm_subscribe($gid, $node, $account) {
  $form['gid'] = array(
    '#type' => 'value',
    '#value' => $gid,
  );
  $form['account'] = array(
    '#type' => 'value',
    '#value' => $account,
  );
  if ($node->og_selective == OG_MODERATED) {
    $form['request'] = array(
      '#type' => 'textarea',
      '#title' => t('Additional details'),
      '#description' => t('Add any detail which will help an administrator decide whether to approve or deny your membership request.'),
    );
  }
  return confirm_form($form, t('Are you sure you want to join the group %title?', array(
    '%title' => $node->title,
  )), 'node/' . $node->nid, ' ', t('Join'), t('Cancel'));
}

/**
 * Confirm og membership submit handler
 */
function og_confirm_subscribe_submit($form_id, $form_values) {
  $return = og_subscribe_user($form_values['gid'], $form_values['account'], $form_values['request']);
  if (!empty($return['message'])) {
    drupal_set_message($return['message']);
  }
  return 'node/' . $form_values['gid'];
}

/**
 * Create a new membership for a given user to given group. Edits to membership should 
 * go through og_save_subscription(). No access control since this is an API function.
 *
 * @return string 'approval', 'subscribed' or 'rejected' depending on the group's configuration.
 **/
function og_subscribe_user($gid, $account, $request = NULL) {

  // moderated groups must approve all members (selective=1)
  $node = node_load($gid);
  switch ($node->og_selective) {
    case OG_MODERATED:
      og_save_subscription($gid, $account->uid, array(
        'is_active' => 0,
      ));
      $sql = og_list_users_sql(1, 1);
      $res = db_query($sql, $node->nid);
      $admins = array();
      while ($row = db_fetch_object($res)) {
        $admins[] = $row->uid;
      }
      if (!empty($admins)) {

        // Prepend user's request text with standard cruft. Should be a
        // variable but that's a bit annoying.
        if ($request) {
          $request = t("\n\nPersonal message from @name:\n------------------\n\n@request", array(
            '@name' => $account->name,
            '@request' => $request,
          ));
        }
        $variables = array(
          '@group' => $node->title,
          '@username' => $account->name,
          '!approve_url' => url("og/approve/{$node->nid}/{$account->uid}", NULL, NULL, TRUE),
          '!group_url' => url("og/users/{$node->nid}", NULL, NULL, TRUE),
        );
        $message = array(
          'subject' => _og_user_mail_text('og_request_user_subject', $variables),
          'body' => _og_user_mail_text('og_request_user_body', $variables) . $request,
        );

        // Send notifications to each admin; Sending an array of recipients
        // implies that this is a bulk message.
        module_invoke_all('og', 'user request', $gid, $admins, $message);
      }
      $return_value = array(
        'type' => 'approval',
        'message' => t('Membership request to the %group group awaits approval by an administrator.', array(
          '%group' => $node->title,
        )),
      );
      break;
    case OG_OPEN:
      og_save_subscription($gid, $account->uid, array(
        'is_active' => 1,
      ));
      $return_value = array(
        'type' => 'subscribed',
        'message' => t('You are now a member of the %group.', array(
          '%group' => $node->title,
        )),
      );
      break;
    case OG_CLOSED:
    case OG_INVITE_ONLY:

      // admins can add members to these groups, but others can't.
      if (og_is_node_admin($node)) {
        og_save_subscription($gid, $account->uid, array(
          'is_active' => 1,
        ));
      }
      else {
        $return_value = array(
          'type' => 'rejected',
          'message' => t('Membership request to the @group group was rejected, only group administrators can add users to this group.', array(
            '@group' => $node->title,
          )),
        );
      }
  }
  return $return_value;
}
function og_unsubscribe($gid, $uid = NULL) {
  global $user;
  if (is_null($uid)) {
    $uid = $user->uid;
  }
  $node = node_load($gid);
  $account = user_load(array(
    'uid' => $uid,
  ));
  if (!in_array($gid, array_keys(og_get_subscriptions($account->uid, 0))) || $uid != $user->uid && !og_is_node_admin($node)) {

    // Ubsubscribee must be a member or awaiting approval
    // Only admins can remove another member.
    drupal_access_denied();
    exit;
  }
  if (!og_is_group_type($node->type) || $node->og_selective == OG_CLOSED && !og_is_node_admin($node) || $node->uid == $uid) {

    // Regular users may not leave from a closed group
    // Group manager may never leave (TODO: fix), such a link should never be generated
    drupal_access_denied();
    exit;
  }
  else {
    return drupal_get_form('og_confirm_unsubscribe', $gid, $node, $uid);
  }
}

/**
 * Confirm og unsubscription form
 */
function og_confirm_unsubscribe($gid, $node, $uid) {
  $form['gid'] = array(
    '#type' => 'value',
    '#value' => $gid,
  );
  $form['uid'] = array(
    '#type' => 'value',
    '#value' => $uid,
  );
  $account = user_load(array(
    'uid' => $uid,
  ));
  return confirm_form($form, t('Are you sure you want to remove !name from the group %title?', array(
    '!name' => theme('username', $account),
    '%title' => $node->title,
  )), 'og/users/' . $node->nid, ' ', t('Remove'), t('Cancel'));
}

/**
 * Confirm og unsubscription submit handler
 */
function og_confirm_unsubscribe_submit($form_id, $form_values) {
  global $user;
  og_delete_subscription($form_values['gid'], $form_values['uid']);
  drupal_set_message(t('User removed from group.'));
  if ($user->uid == $form_values['uid']) {
    return 'og';
  }
  else {
    return "node/" . $form_values['gid'];
  }
}

// since a user's memberships are loaded into $user object, this function is only occassionally useful to get group memberships for users other than the current user
// even then, it often makes sense to call user_load() instead of this function.
// load all memberships for a given user
function og_get_subscriptions($uid, $min_is_active = 1, $reset = FALSE) {
  static $subscriptions = array();
  if ($reset) {
    unset($subscriptions[$uid]);
  }
  if (!isset($subscriptions[$uid][$min_is_active])) {
    list($types, $in) = og_get_sql_args();
    array_unshift($types, $min_is_active);
    array_unshift($types, $uid);
    $sql = "SELECT n.title, n.type, n.status, ou.* FROM {og_uid} ou INNER JOIN {node} n ON ou.nid = n.nid WHERE ou.uid = %d AND ou.is_active >= %d AND n.type {$in} ORDER BY n.title";
    $result = db_query($sql, $types);
    while ($row = db_fetch_array($result)) {
      $subscriptions[$uid][$min_is_active][$row['nid']] = $row;
    }
    if (!isset($subscriptions[$uid][$min_is_active])) {
      $subscriptions[$uid][$min_is_active] = array();
    }
  }
  return $subscriptions[$uid][$min_is_active];
}
function og_list_users_sql($min_is_active = 1, $min_is_admin = 0, $orderby = 'u.name ASC') {
  return "SELECT u.uid, u.name, u.mail, u.picture, ou.* FROM {og_uid} ou INNER JOIN {users} u ON ou.uid = u.uid WHERE ou.nid = %d AND u.status > 0 AND ou.is_active >= {$min_is_active} AND ou.is_admin >= {$min_is_admin} ORDER BY {$orderby}";
}
function og_add_users($gid) {
  $form['og_names'] = array(
    '#type' => 'textarea',
    '#title' => t('List of users'),
    '#rows' => 5,
    '#cols' => 70,
    // no autocomplete b/c user_autocomplete can't handle commas like taxonomy. pls improve core.
    // '#autocomplete_path' => 'user/autocomplete',
    '#description' => t('Add one or more usernames in order to associate users with this group. Multiple usernames should be separated by a comma.'),
  );
  $form['op'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  $form['gid'] = array(
    '#type' => 'value',
    '#value' => $gid,
  );
  return $form;
}
function og_add_users_validate($form_id, $form_values) {
  $names = explode(',', $form_values['og_names']);
  foreach ($names as $name) {
    $account = user_load(array(
      'name' => trim($name),
    ));
    if ($account->uid) {
      $accounts[] = $account;
      $uids[] = $account->uid;
    }
    else {
      $bad[] = check_plain($name);
      $err = TRUE;
    }
  }
  if ($err) {
    form_set_error('og_names', t('Unrecognized %names:', array(
      '%names' => format_plural(count($bad), 'name', 'names'),
    )) . ' ' . implode(', ', $bad));
  }
}
function og_add_users_submit($form_id, $form_values) {

  // safest option is to do a select, filter existing members, then insert
  $names = explode(',', $form_values['og_names']);
  foreach ($names as $name) {
    $account = user_load(array(
      'name' => trim($name),
    ));
    if ($account->uid) {
      $accounts[] = $account;
    }
  }
  foreach ($accounts as $account) {
    og_save_subscription($form_values['gid'], $account->uid, array(
      'is_active' => 1,
    ));
  }
  drupal_set_message(format_plural(count($accounts), '1 user added to the group.', '@count users added to the group.'));
}
function og_list_users_page($gid) {
  $node = node_load($gid);
  $access = og_is_node_admin($node);
  if ($access) {
    $header[] = array(
      'data' => t('Operations'),
      'colspan' => 2,
    );
  }

  // prepend the group manager
  $i = 0;
  $rows[$i][] = array(
    'data' => theme('username', $node) . '&nbsp;<em>' . t('Manager') . '</em>',
    'colspan' => 3,
  );
  $i++;
  $sql = og_list_users_sql(0, 0, 'ou.is_admin DESC, ou.is_active ASC, u.name ASC');

  /* list group admins first, pending members second, regular members last.  Alphabetize within each of these categories */
  $result = pager_query($sql, 500, 0, NULL, $gid);
  while ($account = db_fetch_object($result)) {
    if ($account->uid != $node->uid) {
      $username = theme('username', $account);
      if (!$account->is_active) {
        $username .= '&nbsp;<em>' . t('(approval needed)') . '</em>';
      }
      elseif ($account->is_admin) {
        $username .= '&nbsp;<em>' . t('Administrator') . '</em>';
      }
      $rows[$i][] = $username;
      if ($access) {
        if ($account->is_active) {
          $rows[$i][] = l(t('Remove'), "og/unsubscribe/{$gid}/{$account->uid}", array(), "destination=og/users/{$gid}");
          if ($account->is_admin) {
            $rows[$i][] = l(t('Admin: Remove'), "og/delete_admin/{$gid}/{$account->uid}", array(), 'destination=' . $_GET['q']);
          }
          else {
            $rows[$i][] = l(t('Admin: Create'), "og/create_admin/{$gid}/{$account->uid}", array(), 'destination=' . $_GET['q']);
          }
        }
        else {
          $rows[$i][] = l(t('Approve'), "og/approve/{$gid}/{$account->uid}", array(), "destination=og/users/{$gid}");
          $rows[$i][] = l(t('Deny'), "og/deny/{$gid}/{$account->uid}", array(), "destination=og/users/{$gid}");
        }
      }
      $i++;
    }
  }
  if ($pager = theme('pager', NULL, 500)) {
    $rows[$i][] = array(
      'data' => $pager,
      'colspan' => 2,
    );
  }
  $header = array();
  $output = theme('table', $header, $rows);
  $bc[] = array(
    'path' => "og",
    'title' => t('Groups'),
  );
  $bc[] = array(
    'path' => "node/{$gid}",
    'title' => $node->title,
  );
  menu_set_location($bc);
  drupal_set_title(t('Members') . ': ' . l($node->title, "node/{$node->nid}"));
  return $output;
}
function og_list_users_faces_page($gid, $column_count = 5, $row_count = 10, $pager_id = 0) {
  $max_per_page = $row_count * $column_count;
  $sql = og_list_users_sql(1, 0, 'ou.is_admin DESC, u.picture DESC, u.name ASC');

  /* list group admins first, members with pics second, regular members last.  Alphabetize within each of these categories */
  $result = pager_query($sql, $max_per_page, $pager_id, NULL, $gid);
  $output = theme('og_picture_grid', $result, $column_count);
  $output .= theme('pager', NULL, $max_per_page, $pager_id);
  return $output;
}

// from views_bonus: grid.inc
function theme_og_picture_grid($result, $column_count = 5) {
  $content = '<table class="og-picture-grid">';
  $count = 0;
  $total = db_num_rows($result);
  while ($user = db_fetch_object($result)) {
    $classes = 'og-picture-grid-item';
    if ($count < $column_count) {
      $classes .= ' first';
    }
    $item = '';
    if ($count % $column_count == 0) {
      $content .= '<tr>';
    }
    $picture = theme('user_picture', $user);
    $name = theme('username', $user);
    $group_role = $user->is_admin ? t('admin') : '&nbsp;';
    $content .= "<td class=\"{$classes}\">{$picture}<div class=\"og-name\">{$name}</div>";
    if ($user->is_admin) {
      $txt = t('admin');
      $content .= "<div class=\"group-role\">{$txt}</div>";
    }
    $content .= "</td>\n";
    $count++;
    if ($count % $column_count == 0 || $count == $total) {
      $content .= '</tr>';
    }
  }
  $content .= '</table>';
  if ($total) {
    return $content;
  }
}

// TODO: use Views
function og_opml() {
  $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
  $output .= "<opml version=\"1.1\">\n";
  $output .= "<head>\n";
  $output .= '<title>' . check_plain(variable_get('site_name', 'Drupal')) . "</title>\n";
  $output .= '<dateModified>' . gmdate('r') . "</dateModified>\n";
  $output .= "</head>\n";
  $output .= "<body>\n";
  global $user;
  foreach ($user->og_groups as $gid => $group) {
    $output .= '<outline text="' . check_plain($group['title']) . '" xmlUrl="' . url("node/{$gid}/feed", NULL, NULL, TRUE) . "\" />\n";
  }
  $output .= "</body>\n";
  $output .= "</opml>\n";
  drupal_set_header('Content-Type: text/xml; charset=utf-8');
  print $output;
}
function og_page_activity() {
  $sql = "SELECT oga.group_nid, node_group_nid.title, node_group_nid.uid, u.name, COUNT(*) as ncount, MAX(n.created) as ncreated, SUM(ncs.comment_count) as ccount, MAX(last_comment_timestamp) AS lct FROM {node} n INNER JOIN {og_ancestry} oga ON n.nid = oga.nid LEFT JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {node} node_group_nid ON oga.group_nid = node_group_nid.nid INNER JOIN {users} u ON node_group_nid.uid = u.uid WHERE n.status = 1 GROUP BY oga.group_nid, node_group_nid.title, node_group_nid.uid, u.name";
  $header = array(
    array(
      'data' => t('Title'),
      'field' => 'node_group_nid.title',
    ),
    array(
      'data' => t('Manager'),
      'field' => 'u.name',
    ),
    array(
      'data' => t('Posts'),
      'field' => 'ncount',
    ),
    array(
      'data' => t('Comments'),
      'field' => 'ccount',
    ),
    array(
      'data' => t('Age'),
      'field' => 'node_group_nid.created',
    ),
    array(
      'data' => t('Last comment'),
      'field' => 'lct',
      'sort' => 'asc',
    ),
  );
  $result = db_query($sql . tablesort_sql($header));
  while ($row = db_fetch_object($result)) {
    $rows[] = array(
      l($row->title, "node/{$row->group_nid}"),
      theme('username', $row),
      $row->ncount,
      $row->ccount,
      format_interval(time() - $row->ncreated),
      format_interval(time() - $row->lct),
    );
  }
  return theme('table', $header, $rows);
}

/**
 * When you view a group, you really see some facts about the group in a block and then 
 * lists of nodes affiliated with that group. The node list is provided by the View of 
 * your choice (see admin/og/og). If you use og_panels.module and the group has defined
 * a default home page, then that page becomes the presentation of the GHP.
 *
 * @return void
 **/

//
function og_view_group(&$node, $teaser = FALSE, $page = FALSE) {
  if ($teaser || !$page) {
    if (!empty($node->og_description)) {
      $node->content['og_description'] = array(
        '#type' => 'item',
        '#title' => t('Description'),
        '#value' => check_plain($node->og_description),
      );
    }
  }
  else {
    $bc[] = array(
      'path' => "og",
      'title' => t('Groups'),
    );
    $bc[] = array(
      'path' => "node/{$node->nid}",
      'title' => $node->title,
    );
    menu_set_location($bc);
    unset($node->content['body']);
    $node->content['og_mission'] = array(
      '#value' => $node->body,
      // node_prepare() already ran check_markup()
      '#node' => $node,
      '#weight' => -3,
      '#theme' => 'og_mission',
    );
    $view = views_get_view(variable_get('og_home_page_view', 'og_ghp_ron'));
    $views_available = variable_get('views_defaults', array());
    if ($views_available[$view->name] == "disabled") {

      // Do nothing. Assume the group node type handles homepage, or theme layer - i.e. node-<node_type>.tpl.php
    }
    else {
      $view->url = 'node';
      $args[] = $node->nid;

      // TODO: use own callback for feeds
      if (arg(2) == 'feed') {
        $view->description = $node->og_description;
        $args[] = 'feed';
      }
      $built = views_build_view('embed', $view, $args, $view->use_pager, $view->nodes_per_page);

      // Views will set either total_rows or num_rows depending on whether View has a pager.
      if ($view->total_rows || $view->num_rows || $view->page_empty) {
        $node->content['view'] = array(
          '#value' => $built,
        );
      }
      elseif (empty($_POST)) {

        // Use this default empty text unless overridden by View (which is a usually not a good idea. This text is smart)
        og_home_empty($node);
      }
      drupal_set_title(check_plain(views_get_title($view, 'page')));
    }
  }
}
function og_home_empty($node) {
  global $user;
  $dest = drupal_get_destination();
  if (in_array($node->nid, array_keys($user->og_groups))) {
    $msg = t('No posts in this group.');
  }
  else {
    $msg = t('No public posts in this group.');
    if (!$user->uid) {
      $msg .= ' ' . t('You must <a href="!register">register</a> or <a href="!login">login</a> and become a member in order to post messages, and view any private posts.', array(
        '!register' => url("user/register", $dest),
        '!login' => url("user/login", $dest),
      ));
    }
    elseif ($node->og_selective < OG_INVITE_ONLY) {
      $msg .= ' ' . t('Consider <a href="!url">joining this group</a> in order to view its posts.', array(
        '!url' => url("og/subscribe/{$node->nid}", $dest),
      ));
    }
  }
  drupal_set_message($msg);
}

// A FAPI theme callback. See og_view_group(). Also called from an og_panels content item.
function theme_og_mission($form) {
  return !empty($form['#value']) ? '<div id="mission" class="og-mission">' . $form['#value'] . '</div>' : '';
}

/**
 * Adds standard fields for any node configured to be a group node
 *
 * @param object $node
 */
function og_group_form($node) {
  global $user;
  $edit = $_POST['edit'];
  $form['og_description'] = array(
    '#type' => 'textfield',
    '#title' => t('Description'),
    '#default_value' => $node->og_description,
    '#size' => 70,
    '#maxlength' => 150,
    '#required' => true,
    '#description' => t('A brief description for the group details block and the group directory.'),
    '#weight' => -4,
  );
  if ($node->nid) {
    $default = $node->og_selective;
  }
  else {
    $default = OG_OPEN;
  }
  $form['og_selective'] = array(
    '#type' => 'radios',
    '#title' => t('Membership requests'),
    '#required' => TRUE,
    '#default_value' => $default,
    '#options' => array(
      t('open - membership requests are accepted immediately.'),
      t('moderated - membership requests must be approved.'),
      t('invite only - membership must be created by an administrator.'),
      t('closed - membership is exclusively managed by an administrator.'),
    ),
    '#description' => t('How should membership requests be handled in this group? When you select <em>closed</em>, users will not be able to join <strong>or</strong> leave.'),
  );

  // registration checkbox
  // get the visibility for normal users
  $visibility = variable_get('og_visibility_registration', OG_REGISTRATION_CHOOSE_FALSE);

  // admin can always choose - get right default
  if (user_access('administer nodes')) {
    $visibility = in_array($visibility, array(
      OG_REGISTRATION_NEVER,
      OG_REGISTRATION_CHOOSE_FALSE,
    )) ? OG_REGISTRATION_CHOOSE_FALSE : OG_REGISTRATION_CHOOSE_TRUE;
  }
  $default = FALSE;
  switch ($visibility) {
    case OG_REGISTRATION_NEVER:
      $form['og_register'] = array(
        '#type' => 'value',
        '#value' => 0,
      );
      break;
    case OG_REGISTRATION_ALWAYS:
      $form['og_register'] = array(
        '#type' => 'value',
        '#value' => 1,
      );
      break;
    case OG_REGISTRATION_CHOOSE_TRUE:
      $default = TRUE;

    // fall through
    case OG_REGISTRATION_CHOOSE_FALSE:
      $form['og_register'] = array(
        '#type' => 'checkbox',
        '#title' => t('registration form'),
        '#default_value' => $node->nid ? $node->og_register : $default,
        '#description' => t('Should users be able to join this group during registration?. If checked, a corresponding checkbox will be added to the registration form.'),
      );
      break;
  }

  // directory checkbox
  $visibility = variable_get('og_visibility_directory', OG_DIRECTORY_CHOOSE_TRUE);

  // override for admins - get right default
  if (user_access('administer nodes')) {
    $visibility = in_array($visibility, array(
      OG_DIRECTORY_NEVER,
      OG_DIRECTORY_CHOOSE_FALSE,
    )) ? OG_DIRECTORY_CHOOSE_FALSE : OG_DIRECTORY_CHOOSE_TRUE;
  }
  $default = FALSE;
  switch ($visibility) {
    case OG_DIRECTORY_NEVER:
      $form['og_directory'] = array(
        '#type' => 'value',
        '#value' => 0,
      );
      break;
    case OG_DIRECTORY_ALWAYS:
      $form['og_directory'] = array(
        '#type' => 'value',
        '#value' => 1,
      );
      break;
    case OG_DIRECTORY_CHOOSE_TRUE:
      $default = TRUE;

    // fall through
    case OG_DIRECTORY_CHOOSE_FALSE:
      $form['og_directory'] = array(
        '#type' => 'checkbox',
        '#title' => t('list in groups directory'),
        '#default_value' => $node->nid ? $node->og_directory : $default,
        '#description' => t('Should this group appear on the !page?  Disabled if the group is set to <em>private group</em>.', array(
          '!page' => l(t('list of groups page'), 'og'),
        )),
      );
      break;
  }

  // language
  if (module_exists('locale') && ($languages = locale_supported_languages())) {
    if (count($languages['name']) > 1) {
      $languages['name'] = array_map('check_plain', $languages['name']);
      $form['locale']['og_language'] = array(
        '#type' => 'radios',
        '#title' => t('Language'),
        '#default_value' => $node->og_language,
        '#options' => $languages['name'],
        '#description' => t('Selecting a different locale will change the interface language of the group. Users who have chosen a preferred language always see their chosen language.'),
      );
    }
  }
  if ($theme_form = system_theme_select_form(t('Selecting a different theme will change the look and feel of the group.'), $edit['theme'] ? $edit['theme'] : $node->og_theme, 2)) {
    $theme_form['themes']['#weight'] = 8;
    $form += $theme_form;
  }
  return $form;
}

// returns all the group affiliations for a given node.
function og_get_node_groups($node) {
  $groups = array();
  if (!og_is_group_type($node->type)) {
    $result = og_get_node_groups_result($node->nid);
    while ($row = db_fetch_object($result)) {
      $groups[$row->group_nid] = $row->title;
    }
    return $groups;
  }
}

// Just the query for the get_node_groups function. Is reused in og_views.inc
function og_get_node_groups_result($nid) {

  // We do not run db_rewrite_sql() here since we need to know about groups that the user cannot access as well (i.e. node edit).
  $sql = "SELECT oga.group_nid, n.title FROM {node} n INNER JOIN {og_ancestry} oga ON n.nid = oga.group_nid WHERE oga.nid = %d";
  return db_query($sql, $nid);
}
function og_submit_group(&$node) {

  // comments are not allowed on group nodes, since we don't have any nice way to present them
  if (og_is_group_type($node->type)) {
    $node->comment = COMMENT_NODE_DISABLED;
  }
  elseif (!empty($node->og_groups_inaccessible)) {

    // Add the inaccessible groups which did not show in Audience selector
    $node->og_groups = (array) $node->og_groups + $node->og_groups_inaccessible;
  }

  // Change $node->theme to $node->og_theme so it matches how we node_load(). the node form uses $theme and not og_theme
  // If author chose the default theme, then '' is written to the DB and group will follow any change made by site admin.
  if (isset($node->theme)) {
    $node->og_theme = $node->theme;
  }

  // Normalize og_groups array
  if (is_array($node->og_groups)) {
    $node->og_groups = array_filter($node->og_groups);
    $node->og_groups = array_keys($node->og_groups);
  }
}
function og_load_group(&$node) {
  $sql = 'SELECT selective AS og_selective, description AS og_description, theme AS og_theme, register AS og_register, directory AS og_directory, language AS og_language, private AS og_private FROM {og} WHERE nid = %d';
  $result = db_query($sql, $node->nid);
  $node = (object) array_merge((array) $node, (array) db_fetch_array($result));
  $node->comment = COMMENT_NODE_DISABLED;

  // we don't use comments on og nodes. technically not needed since we set this on node submit
}
function og_insert_group($node) {
  $sql = "INSERT INTO {og} (nid, theme, selective, description, register, directory, language, private) VALUES (%d, '%s', %d, '%s', %d, %d, '%s', %d)";
  db_query($sql, $node->nid, $node->og_theme, $node->og_selective, $node->og_description, $node->og_register, $node->og_directory, $node->og_language, $node->og_private);
}
function og_update_group($node) {
  $sql = "UPDATE {og} SET theme = '%s', selective = %d, register = %d, description = '%s', directory = %d, language = '%s', private = %d WHERE nid = %d";
  db_query($sql, $node->og_theme, $node->og_selective, $node->og_register, $node->og_description, $node->og_directory, $node->og_language, $node->og_private, $node->nid);
  if (!db_affected_rows()) {
    og_insert_group($node);
  }
}

/**
 * Implementation of hook_nodeapi().
 *
*/
function og_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  global $user;
  switch ($op) {
    case 'view':
      $group_node = og_get_group_context();
      if ($group_node && $page && $node->og_groups) {

        // set breadcrumb and title on non group nodes
        $bc[] = array(
          'path' => "og",
          'title' => t('Groups'),
        );
        $bc[] = array(
          'path' => "node/{$group_node->nid}",
          'title' => $group_node->title,
        );
        $bc[] = array(
          'path' => "node/{$node->nid}",
          'title' => $node->title,
        );
        menu_set_location($bc);
      }
      if (og_is_group_type($node->type)) {
        og_view_group($node, $teaser, $page);
      }
      break;
    case 'load':
      if (og_is_group_type($node->type)) {
        og_load_group($node);
      }
      if ($grps = og_get_node_groups($node)) {

        // TODO: Refactor so we don't need 2 arrays.
        $node->og_groups = array_keys($grps);
        $node->og_groups_both = $grps;
        $public = db_result(db_query_range("SELECT is_public FROM {og_ancestry} WHERE nid = %d", $node->nid, 0, 1));
        $node->og_public = $public ? TRUE : FALSE;
      }
      break;
    case 'validate':

      // Ensure that a group is selected if groups are required. needed when author has no groups. In other cases, fapi does the validation
      if (og_is_group_post_type($node->type) && variable_get('og_audience_required', FALSE) && !user_access('administer nodes')) {
        if (!isset($node->og_groups)) {
          form_set_error('title', t('You must !join before posting on this web site.', array(
            '!join' => l(t('join a group'), 'og'),
          )));
        }
      }
      break;
    case 'submit':
      og_submit_group($node);
      break;
    case 'prepare':

      // TODO: some people don't like forcing comments to disabled for groups.
      if (og_is_group_type($node->type)) {
        $node->comment = COMMENT_NODE_DISABLED;
      }
      break;
    case 'delete':
      $sql = "DELETE FROM {og} WHERE nid = %d";
      db_query($sql, $node->nid);
      $sql = "DELETE FROM {og_ancestry} WHERE nid = %d";
      db_query($sql, $node->nid);
      $sql = "DELETE FROM {og_uid} WHERE nid = %d";
      db_query($sql, $node->nid);
      break;
    case 'insert':
      if (og_is_group_type($node->type)) {
        og_insert_group($node);

        // make sure the node owner is a full powered member
        og_save_subscription($node->nid, $node->uid, array(
          'is_active' => 1,
          'is_admin' => 1,
        ));

        // load new group into $user->og_groups so that author can get redirected to the new group
        if ($node->uid == $user->uid) {
          $user->og_groups = og_get_subscriptions($node->uid, 1, TRUE);
        }
        $account = user_load(array(
          'uid' => $node->uid,
        ));
        $variables = array(
          '@group' => $node->title,
          '!group_url' => url("node/{$node->nid}", NULL, NULL, TRUE),
          '@username' => $account->name,
          '!invite_url' => url("og/invite/{$node->nid}", NULL, NULL, TRUE),
        );
        $message = array(
          'subject' => _og_user_mail_text('og_new_admin_subject', $variables),
          'body' => _og_user_mail_text('og_new_admin_body', $variables),
        );

        // Alert the user that they are now the admin of the group.
        module_invoke_all('og', 'admin new', $gid, $account->uid, $message);
      }
      else {
        og_save_ancestry($node);
      }
      break;
    case 'update':
      if (og_is_group_type($node->type)) {
        og_update_group($node);

        // make sure the node owner is a full powered member
        og_save_subscription($node->nid, $node->uid, array(
          'is_active' => 1,
          'is_admin' => 1,
        ));

        // load new group into $user->og_groups so that author can get redirected to the new group
        if ($node->uid == $user->uid) {
          $user->og_groups = og_get_subscriptions($account->uid, 1, TRUE);
        }
      }
      else {
        og_save_ancestry($node);
      }
      break;
    case 'search result':

      // TODO: add group info
      break;
    case 'rss item':
      if ($node->og_groups) {
        foreach ($node->og_groups_both as $gid => $title) {

          // TODO: should be absolute link. core bug.
          $append['og_links'] = array(
            'title' => $title,
            'href' => "node/{$gid}",
          );
          $ret[] = array(
            'key' => 'group',
            'value' => check_plain($title),
            'attributes' => array(
              'domain' => url("node/{$gid}", NULL, NULL, TRUE),
              'xmlns' => 'http://drupal.org/project/og',
            ),
          );
        }

        // to get these modifications to work on 4.7, one needs to patch as per http://drupal.org/node/41703
        $node->body .= '<div class="og_rss_groups">' . theme('links', $append) . '</div>';
        $node->teaser .= '<div class="og_rss_groups">' . theme('links', $append) . '</div>';
        return $ret;
      }
      break;
  }
}
function og_msgid_server() {
  global $base_url;
  if ($dir = str_replace("/", ".", substr(strchr(str_replace("http://", "", $base_url), "/"), 1))) {
    $at = "@{$dir}." . $_SERVER['SERVER_NAME'];
  }
  else {
    $at = '@' . $_SERVER['SERVER_NAME'];
  }
  return strtolower($at);
}
function og_form_alter($form_id, &$form) {

  // Add audience selection to node forms
  if (isset($form['#node']) && $form_id == $form['#node']->type . '_node_form') {
    $node = $form['#node'];
    if (og_is_group_type($node->type)) {
      $form = array_merge($form, og_group_form($node));

      // Don't trample on custom label.
      if ($form['body_filter']['body']['#title'] == t('Body')) {
        $form['body_filter']['body']['#title'] = t('Mission statement');
        $form['body_filter']['body']['#description'] = t('A welcome greeting for your group home page. Consider listing the group objectives and mission.');
      }
      $form['author']['name']['#title'] = t('Group manager');
      $form['options']['sticky']['#title'] = t('Sticky at top of group home page and other lists');
    }
    elseif (og_is_group_post_type($node->type)) {
      if (!$node->nid) {
        if ($group_node = og_get_group_context()) {
          $bc[] = array(
            'path' => 'og',
            'title' => t('Groups'),
          );
          $bc[] = array(
            'path' => "node/{$group_node->nid}",
            'title' => $group_node->title,
          );
          $bc[] = array(
            'path' => "node/add/{$node->type}",
            'title' => t('Create foo'),
          );

          //TODO fix title

          // TODO: not working for unknown reason
          // menu_set_location($bc);
        }
      }
      og_form_add_og_audience($form_id, $form);
    }
  }

  // add option to migrate messages before deleting a group
  // TODO: add option to move memberships as well
  if ($form_id == 'node_delete_confirm') {
    $node = node_load($form['nid']['#value']);
    if (og_is_group_type($node->type)) {
      og_node_delete_group_form($form);
    }
    else {
      og_node_delete_nongroup_form($form);
    }
  }
  if ($form_id == 'views_edit_view') {
    $form['page-info']['url']['#description'] .= '<p>' . t("To display a View as a tab on your Organic Groups group home pages, set the url to 'node/\$group/custom' (where <em>custom</em> is whatever you wish). Then open Page >> Menu and check <em>Provide Menu</em> and <em>Provide Menu as Tab</em>; also make the first argument in the View be the <em>OG: Group nid(s)</em> argument. The <em>\$group</em> path element is a placeholder for the group nid and it ensures that the tab <strong>only</strong> appears on OG group nodes.") . '</p>';
  }
  if ($form_id == 'node_type_form') {

    // Built in content types do not alow changes to type machine name.
    if (isset($form['identity']['type']['#default_value'])) {
      $usage = variable_get('og_content_type_usage_' . $form['identity']['type']['#default_value'], 'omitted');
    }
    else {
      $usage = variable_get('og_content_type_usage_' . $form['identity']['type']['#value'], 'omitted');
    }

    // Persist $usage so that we can rebuild node access as needed.
    $form['old_og_content_type_usage'] = array(
      '#type' => 'value',
      '#value' => $usage,
    );
    $form['#submit']['og_node_type_form_submit'] = array();
    $options = og_types_map();
    $form['workflow']['og_content_type_usage'] = array(
      '#type' => 'radios',
      '#title' => t('Organic groups usage'),
      '#default_value' => $usage,
      '#options' => $options,
      '#description' => t('Specify how organic groups should treat nodes of this type. Nodes may behave as a group, as group posts, or may not participate in organic groups at all.'),
    );
  }
}

// Rebuild node access if Usage has changed
function og_node_type_form_submit($form_id, $form_values) {
  $type = $form_values['type'];
  $var = 'og_content_type_usage';
  $new = $form_values[$var];
  $old = $form_values['old_' . $var];
  if ($new != $old) {

    /**
     * Rebuild the node access database *for a single content type*.
     * TODO: older versions of mySQL can't do subselect.
     */
    db_query("DELETE FROM {node_access} WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", $type);

    // If not in 'safe mode', increase the maximum execution time:
    if (!ini_get('safe_mode')) {
      set_time_limit(240);
    }
    $result = db_query("SELECT nid FROM {node} WHERE type = '%s'", $type);
    while ($node = db_fetch_object($result)) {
      $loaded_node = node_load($node->nid, NULL, TRUE);

      // To preserve database integrity, only aquire grants if the node
      // loads successfully.
      if (!empty($loaded_node)) {
        node_access_acquire_grants($loaded_node);
      }
    }
    cache_clear_all();
    drupal_set_message(t('The node access table was rebuilt for %type posts.', array(
      '%type' => node_get_types('name', $type),
    )));
  }
}

// form_alter() the node_delete form for a group
function og_node_delete_group_form(&$form) {
  $options[] = t('Do nothing.');
  $options[] = t("Delete all group posts which don't also belong to another group.");
  if (user_access('administer nodes')) {
    $options[] = t('Move all group posts to the group listed below.');
  }
  $form['verb'] = array(
    '#type' => 'radios',
    '#title' => t('Group posts'),
    '#options' => $options,
    '#weight' => 0,
    '#description' => t('In addition to deleting this group, you may choose how to disposition the posts within it.'),
  );
  if (user_access('administer nodes')) {
    $options = og_all_groups_options();
    unset($options[$form['nid']['#value']]);
    $form['target'] = array(
      '#type' => 'select',
      '#title' => t('Target group'),
      '#default_value' => 0,
      '#options' => $options,
      '#weight' => 0,
      '#description' => t('If you chose <b>Move all group posts</b> above, specify a destination group.'),
    );
  }

  // register a submit handler
  $form['#submit']['og_node_delete_confirm_submit'] = array();
  $form['actions']['submit']['#value'] = t('Delete group');
}

// form_alter() the node_delete form for a non-group
// redirect back to group home page after a delete
function og_node_delete_nongroup_form(&$form) {
  if ($groupnode = og_get_group_context()) {
    $form['#redirect'] = "node/{$groupnode->nid}";
  }
}

// return all node ids belonging to a group.
// if you are retrieving for displaying, you should a line like below instead of this function:
// $info = views_build_view('items', $view, array($group_nid));
function og_group_child_nids($group_nid) {
  $result = db_query('SELECT oga.nid FROM {og_ancestry} oga WHERE oga.group_nid = %d', $group_nid);
  $child_nids = array();
  while ($row = db_fetch_object($result)) {
    $child_nids[] = $row->nid;
  }
  return $child_nids;
}

// Submit handler for node delete form. handles deletes to group nodes.
function og_node_delete_confirm_submit($form_id, $form_values) {
  $deleted_group_nid = $form_values['nid'];
  $target_group_nid = $form_values['target'];
  $move_children = $form_values['verb'] == 2;
  $delete_orphans = $form_values['verb'] == 1;
  foreach (og_group_child_nids($deleted_group_nid) as $child_nid) {
    $node = node_load($child_nid);
    unset($node->og_groups[$deleted_group_nid]);
    if ($move_children) {

      // There is an array_unique() in og_save_ancestry() which guards against duplicates so don't worry here.
      $node->og_groups[$target_group_nid] = $target_group_nid;
    }
    if ($delete_orphans && count($node->og_groups) == 0) {
      node_delete($node->nid);
    }
    else {
      node_save($node);
    }
  }
  if ($move_children) {
    return 'node/' . $target_group_nid;
  }
}

// return an array containing all groups. suitable for a form item
function og_all_groups_options() {
  list($types, $in) = og_get_sql_args();
  $sql = "SELECT n.title, n.nid FROM {node} n WHERE n.type {$in} AND n.status = 1 ORDER BY n.title ASC";
  $result = db_query($sql, $types);
  while ($row = db_fetch_object($result)) {
    $options[$row->nid] = $row->title;
  }
  return $options ? $options : array();
}

/**
 * Iterate over a set of groups and separate out those that are inaccessible to the current user. 
 * Don't return groups of which the current user is a member, unless $exclude_joined=FALSE
 * is passed. Used in og_form_add_og_audience() and node.tpl.php.
 * 
 * @return array
 *   A two element array containing 'accessible' and 'inaccessible' keys.
 **/
function og_node_groups_distinguish($groups_names_both, $exclude_joined = TRUE) {
  global $user;
  $current_groups = array(
    'accessible' => array(),
    'inaccessible' => array(),
  );
  if (empty($groups_names_both)) {

    // Do nothing.
  }
  else {
    $placeholders = array_fill(0, count($groups_names_both), '%d');
    $sql = 'SELECT n.nid FROM {node} n WHERE n.nid IN (' . implode(', ', $placeholders) . ')';
    $result = db_query(db_rewrite_sql($sql), array_keys($groups_names_both));
    while ($row = db_fetch_object($result)) {
      $current_groups['accessible'][$row->nid]['title'] = $groups_names_both[$row->nid];
    }
    foreach ($groups_names_both as $gid => $title) {
      if (!in_array($gid, array_keys($current_groups['accessible']))) {
        $current_groups['inaccessible'][$gid] = $gid;
      }
    }
    if ($exclude_joined) {

      // Don't return groups that the user has already joined (default).
      $current_groups['accessible'] = array_diff_assoc($current_groups['accessible'], $user->og_groups);
    }
  }
  return $current_groups;
}

/**
 * Helper method to add OG audience fields to a given form. This is
 * lives in a separate function from og_form_alter() so it can be shared
 * by other OG contrib modules.
 */
function og_form_add_og_audience($form_id, &$form) {
  global $user;

  // determine the selected groups if fapi doesn't tell us.
  if ($_SERVER["REQUEST_METHOD"] == 'GET') {
    $gids = $_GET['gids'];
  }
  elseif ($_POST['og_groups_hidden']) {
    $gids = unserialize($_POST['og_groups_hidden']);
  }
  $node = $form['#node'];
  $required = variable_get('og_audience_required', 0) && !user_access('administer nodes');

  // Determine the list of groups that are shown.
  // Start by collecting all groups that the user is a member of.
  $subs = og_get_subscriptions($user->uid);
  $options = array();
  foreach ($subs as $key => $val) {
    $options[$key] = $val['title'];
  }
  if (user_access('administer nodes')) {

    // Node admins see all of groups.
    $all = og_all_groups_options();
    $other = array_diff_assoc($all, $options);

    // Use an optgroup if admin is not a member of all groups.
    if ($other) {
      $options = array(
        t('My groups') => $options,
        t('Other groups') => $other,
      );
      $is_optgroup = TRUE;
    }
    else {
      $options = $all;
    }
  }
  else {

    // Classify those groups which the node already has but the author does not.
    $current_groups = og_node_groups_distinguish($node->og_groups_both);

    // Put inaccessible groups in the $form so that they can persist. See og_submit_group()
    $form['og_invisible']['og_groups_inaccessible'] = array(
      '#type' => 'value',
      '#value' => $current_groups['inaccessible'],
    );

    // Add the accessible groups that they node already belongs to.
    if ($current_groups['accessible']) {

      // Use an optgroup to distinguish between my memberships and additional groups in the Audience select.
      // There is code below which assumes that $options does not have optgroups but that code is within a $simple check
      // So we are OK as long as $simple does not apply to node edits.
      // NOTE: If you form_alter the audience element, beware that it can sometimes be an optgroup.
      foreach ($current_groups['accessible'] as $key => $val) {
        $other[$key] = $val['title'];
      }
      $options = array(
        t('My groups') => $options,
        t('Other groups') => $other,
      );
      $is_optgroup = TRUE;
    }
    else {
      $is_optgroup = FALSE;
    }
  }

  // Show read only item if we are non-admin, and in simple mode (i.e. non-checkboxes) and at least one group is in querystring
  $simple = !user_access('administer organic groups') && !variable_get('og_audience_checkboxes', TRUE) && count($gids);

  // determine value of audience multi-select
  if (count($options) == 1 && $required) {
    $gids = array_keys($options);
    $gid = $gids[0];
    $groups = array(
      $gid,
    );

    // also show read only mode if user has 1 option and we are in required mode
    $simple = TRUE;
  }
  elseif ($gids) {

    // populate field from the querystring if sent
    $groups = $gids;
    if (!user_access('administer nodes') && $simple) {

      // filter out any groups where author is not a member. we cannot rely on fapi to do this when in simple mode.
      $groups = array_intersect($gids, array_keys($options));
    }
  }
  elseif ($node->nid || $node->og_groups) {
    $groups = $node->og_groups;
  }
  else {
    $groups = array();
  }

  // This is only used by og_access module right now.
  $form['og_initial_groups'] = array(
    '#type' => 'value',
    '#value' => $groups,
  );

  // Emit the audience form element.
  if ($simple) {

    // 'simple' mode. read only.
    if (count($groups)) {
      foreach ($groups as $gid) {
        $titles[] = check_plain($options[$gid]);
        $item_value = implode(', ', $titles);
      }
      $form['og_nodeapi']['visible']['og_groups_visible'] = array(
        '#type' => 'item',
        '#title' => t('Audience'),
        '#value' => $item_value,
      );
      $assoc_groups = drupal_map_assoc($groups);

      // this hidden element persists audience values during a Preview cycle. avoids errors on Preview.
      $form['og_nodeapi']['invisible']['og_groups_hidden'] = array(
        '#type' => 'hidden',
        '#value' => serialize($assoc_groups),
      );

      // this 'value' element persists the audience value during submit process
      $form['og_nodeapi']['invisible']['og_groups'] = array(
        '#type' => 'value',
        '#value' => $assoc_groups,
      );
    }
  }
  elseif ($cnt = count($options, COUNT_RECURSIVE)) {

    // show multi-select. if less than 20 choices, use checkboxes.
    $type = $cnt >= 20 || $is_optgroup ? 'select' : 'checkboxes';
    $form['og_nodeapi']['visible']['og_groups'] = array(
      '#type' => $type,
      '#title' => t('Audience'),
      '#attributes' => array(
        'class' => 'og-audience',
      ),
      '#options' => $options,
      '#required' => $required,
      '#description' => format_plural(count($options), 'Show this post in this group.', 'Show this post in these groups.'),
      '#default_value' => $groups,
      '#required' => $required,
      '#multiple' => TRUE,
    );
  }
  else {
    if ($required) {
      form_set_error('title', t('You must !join before posting a %type.', array(
        '!join' => l(t('join a group'), 'og'),
        '%type' => node_get_types('name', $node->type),
      )));
    }
  }
}

/**
 * Similar to node_view() but without calling theme('node').
 *
 * This is needed to get the proper body or teaser for nodes (e.g. Event,
 * Location, CCK node types) and to clean up the body for use in notifications.
 *
 * @param $node
 *   The node object you want to view.
 * @param $teaser
 *   Boolean to select the teaser or the body of the node.
 *
 * @return
 *   The requested data, filtered and ready for use in an e-mail.
 *
 * @see node_view()
 * @see og_mail()
 */
function og_node_view($node, $teaser = FALSE) {

  // Remove the delimiter (if any) that separates the teaser from the body.
  // TODO: this strips legitimate uses of '<!--break-->' also.
  $node->body = str_replace('<!--break-->', '', $node->body);

  // The 'view' hook can be implemented to overwrite the default function
  // to display nodes.
  if (node_hook($node, 'view')) {
    node_invoke($node, 'view', $teaser, TRUE);
  }
  else {
    $node = node_prepare($node, $teaser);
  }

  // Allow modules to change $node->body before viewing.
  node_invoke_nodeapi($node, 'view', $teaser, TRUE);
  return $teaser ? $node->teaser : $node->body;
}

/**
 * Define all OG message strings.
 * Modelled after Drupal's user.module
 */
function _og_user_mail_text($messageid, $variables = array()) {

  // Check if an admin setting overrides the default string.
  if ($admin_setting = variable_get($messageid, FALSE)) {
    return strtr($admin_setting, $variables);
  }
  else {
    switch ($messageid) {
      case 'og_new_node_subject':
        return t("@group: '@title' at @site", $variables);
      case 'og_new_node_body':
        return t("@type '@subject' by @username\n\n@node_teaser\n\n!read_more: !content_url\nPost reply: !reply_url\n\n--\nYou are subscribed from the group '@group' at @site.\nTo manage your subscription, visit !group_url.", $variables);
      case 'og_admin_email_body':
        return t("@body\n\n--\nThis message was sent by an administrator in the '@group' group at @site. To visit this group, browse to !url_group. To unsubscribe from this group, visit !url_unsubscribe.", $variables);
      case 'og_approve_user_subject':
        return t("Membership request approved for '@title'", $variables);
      case 'og_approve_user_body':
        return t("You may now post messages in this group located at !group_url.", $variables);
      case 'og_deny_user_subject':
        return t("Membership request denied for '@title'.", $variables);
      case 'og_deny_user_body':
        return t("Sorry, your membership request was denied.", $variables);
      case 'og_invite_user_subject':
        return t("Invitation to join the group '@group' at @site.", $variables);
      case 'og_invite_user_body':
        return t("Hi. I'm a member of '@group' and I welcome you to join this group as well. Please see the link and message below.\n\n@group\n@description\nJoin: !group_url\n@body", $variables);
      case 'og_request_user_subject':
        return t("Membership request for '@group' from '@username'.", $variables);
      case 'og_request_user_body':
        return t("To instantly approve this request, visit !approve_url.\nYou may deny this request or manage members at !group_url.", $variables);
      case 'og_new_admin_subject':
        return t("You are now an administrator for the group '@group'.", $variables);
      case 'og_new_admin_body':
        return t("@username, you are now an administrator for the group '@group'.\n\nYou can administer this group by logging in here:\n !group_url", $variables);
    }
  }
}

// helper function for queries that need all group types
function og_get_sql_args() {
  if ($types = og_get_types('group')) {
    $in = 'IN (' . implode(', ', array_fill(0, count($types), "'%s'")) . ')';
  }
  else {
    $in = 'IS NULL';
  }
  return array(
    $types,
    $in,
  );
}

/*
 * Determine whether user can act as a group administrator for a given group.
 */
function og_is_node_admin($node) {
  global $user;
  return og_is_group_type($node->type) && (user_access('administer nodes') || !empty($user->og_groups[$node->nid]['is_admin']));
}
function og_user($op, $edit, &$account, $category = NULL) {
  global $user;
  switch ($op) {
    case 'register':
      $options = array();
      list($types, $in) = og_get_sql_args();

      // if groups are passed on querystring, just use them.
      if ($gids = $_REQUEST['gids']) {
        $default_value = $gids;
        foreach ($gids as $gid) {
          $nids[] = (int) $gid;
        }
        $in_nids = implode(', ', array_fill(0, count($nids), "%d"));
        $sql = "SELECT n.nid, n.title, o.* FROM {node} n INNER JOIN {og} o ON n.nid = o.nid WHERE n.type {$in} AND n.status = 1 AND n.nid IN ({$in_nids}) ORDER BY n.title";
        $result = db_query(db_rewrite_sql($sql), array_merge($types, $nids));
      }
      else {
        $default_value = array();

        // perhaps this should be a View
        $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, o.* FROM {node} n INNER JOIN {og} o ON n.nid = o.nid WHERE n.type {$in} AND n.status = 1 AND o.register = 1 ORDER BY n.title"), $types);
      }
      while ($group = db_fetch_object($result)) {
        $options[$group->nid] = '<span class="og-registration-' . $group->nid . '">' . t('Join %name.', array(
          '%name' => $group->title,
        )) . "</span>\n";
        if ($group->selective) {
          $options[$group->nid] .= ' ' . t('(approval needed)');
        }
      }
      if (count($options)) {
        $form['og_register'] = array(
          '#type' => 'fieldset',
          '#title' => t('Groups'),
        );
        $form['og_register']['og_register'] = array(
          '#type' => 'checkboxes',
          '#options' => $options,
          '#default_value' => $default_value,
        );
      }
      return $form;
    case 'insert':
      if (is_array($edit['og_register'])) {
        foreach (array_keys(array_filter($edit['og_register'])) as $gid) {
          $return = og_subscribe_user($gid, $account);
          if (!empty($return['message'])) {
            drupal_set_message($return['message']);
          }
        }
      }
      break;
    case 'delete':
      $sql = 'DELETE FROM {og_uid} WHERE uid=%d';
      db_query($sql, $account->uid);
      break;
    case 'load':
      $account->og_groups = og_get_subscriptions($account->uid);
      break;
    case 'view':

      // only show list of groups to self and admins
      if ($account->uid == $user->uid || user_access('administer organic groups')) {
        if ($account->og_groups) {
          foreach ($account->og_groups as $key => $val) {
            $links[$key] = l($val['title'], "node/{$key}") . theme('og_format_subscriber_status', $val);
          }
          return array(
            t('Groups') => array(
              array(
                'value' => theme('item_list', $links),
                'title' => '',
                'class' => 'og_groups',
              ),
            ),
          );
        }
      }
      break;
  }
}
function og_save_ancestry($node) {
  if (og_is_group_post_type($node->type)) {
    $sql = "DELETE FROM {og_ancestry} WHERE nid = %d";
    db_query($sql, $node->nid);
    if (is_array($node->og_groups)) {
      $node->og_groups = array_unique($node->og_groups);
      foreach ($node->og_groups as $gid) {
        $sql = "INSERT INTO {og_ancestry} (nid, group_nid, is_public) VALUES (%d, %d, %d)";
        db_query($sql, $node->nid, $gid, $node->og_public);
      }
    }
  }
}

/**
 * An implementation of hook_node_type. Automatically update admin preferences when node type is renamed or removed.
 */
function og_node_type($op, $info) {
  switch ($op) {
    case 'delete':
      variable_del('og_content_type_usage_' . $info->type);
      break;
    case 'update':
      $usage = variable_get('og_content_type_usage_' . $info->type, 'omitted');
      variable_set('og_content_type_usage_' . $info->type, $usage);
      variable_del('og_content_type_usage_' . $info->old_type);
  }
}
function og_types_map() {
  $usages = array(
    'group' => t('Group node'),
    'omitted' => t('May not be posted into a group.'),
    'group_post_standard' => t('Standard group post (typically only author may edit).'),
  );
  if (module_exists('og_access')) {
    $usages['group_post_wiki'] = t('Wiki group post (any group member may edit).');
  }
  return $usages;
}

// Return all content types which meet a specified usage.
function og_get_types($usage) {
  $types = node_get_types();
  foreach ($types as $type) {
    if ($usage == 'group_post') {
      if (!og_is_omitted_type($type->type) && !og_is_group_type($type->type)) {
        $return[$usage][] = $type->type;
      }
    }
    else {
      $type_usage = variable_get('og_content_type_usage_' . $type->type, 'omitted');
      $return[$type_usage][] = $type->type;
    }
  }
  return isset($return[$usage]) ? $return[$usage] : array();
}

// returns TRUE if node type lets all subscribers edit the node.
function og_is_wiki_type($type) {
  $usage = variable_get('og_content_type_usage_' . $type, 'omitted');
  return strpos($usage, 'wiki') ? TRUE : FALSE;
}

// returns TRUE if node type can be posted into a group.
function og_is_group_post_type($type) {
  $usage = variable_get('og_content_type_usage_' . $type, 'omitted');
  return strpos($usage, 'group_post') !== FALSE ? TRUE : FALSE;
}
function og_is_omitted_type($type) {
  return variable_get('og_content_type_usage_' . $type, 'omitted') == 'omitted' ? TRUE : FALSE;
}

/**
 * API function for determining whether a given node type has been designated 
 * by admin to behave as a group node (i.e. a container).
 *
 * @param string $type
 * @return boolean
 */
function og_is_group_type($type) {
  return variable_get('og_content_type_usage_' . $type, 'omitted') == 'group' ? TRUE : FALSE;
}

/**
 * Menu callback. Handle old feed urls with permanent redirect. Hopefully rss readers respect that.
 * Don't bother with the overhead of drupal_goto()
 */
function og_feed($gid) {
  header("HTTP/1.1 301 Moved Permanently");
  header('Location: ' . url("node/{$gid}/feed", NULL, NULL, TRUE));
}

/**
 * Check a user's membership in a group. Site admins always return TRUE.
 */
function og_is_group_member($gid, $user) {
  $groups = array_keys($user->og_groups);
  return user_access('administer nodes') || in_array($gid, $groups) ? TRUE : FALSE;
}

/**
 * Implementation of hook_block().
 */
function og_block($op = 'list', $delta = 0, $edit = array()) {
  if ($op == 'list') {
    $blocks[0]['info'] = t('Group details');

    // $blocks[1] used to be the album block. We do not change the numbers to not confuse people who update.
    $blocks[2]['info'] = t('Group members');
    $blocks[3]['info'] = t('New groups');

    // Now provided by og_views. Please don't reuse this number 4
    // $blocks[4]['info'] = t('My groups');
    $blocks[5]['info'] = t('Group notifications');
    $blocks[6]['info'] = t('Group network');

    // Auto-enable the group blocks for fresh installations.
    $blocks[0]['status'] = 1;
    $blocks[0]['weight'] = -2;
    $blocks[5]['status'] = 1;
    $blocks[5]['weight'] = -1;
    return $blocks;
  }
  elseif ($op == 'view') {
    switch ($delta) {
      case 0:
        return og_block_details();
      case 2:
        return og_block_subscribers();
      case 3:
        return og_block_new();
      case 5:
        return og_block_notifications();
      case 6:
        return og_block_users_network();
    }
  }
  elseif ($op == 'configure') {
    switch ($delta) {
      case 2:
        $items['og_block_cnt'] = array(
          '#type' => 'textfield',
          '#title' => t('Maximum number of members to show'),
          '#default_value' => variable_get("og_block_cnt_{$delta}", 10),
          '#size' => 5,
        );
        $items['og_block_subscribers_is_admin'] = array(
          '#type' => 'checkboxes',
          '#title' => t('Group roles'),
          '#default_value' => variable_get("og_block_subscribers_is_admin", array(
            'members',
            'admins',
          )),
          '#options' => array(
            'members' => t('Standard members'),
            'admins' => t('Administrators'),
          ),
          '#description' => t('You may specify which types of group members appear in the listing.'),
        );
        return $items;
      case 3:
        return array(
          'og_block_cnt' => array(
            '#type' => 'textfield',
            '#title' => t('Maximum number of groups to show'),
            '#default_value' => variable_get("og_block_cnt_{$delta}", 10),
            '#size' => 5,
            '#maxlength' => 255,
          ),
        );
    }
  }
  elseif ($op == 'save') {
    switch ($delta) {
      case 2:
        if (isset($edit['og_block_subscribers_is_admin'])) {
          variable_set("og_block_subscribers_is_admin", array_filter($edit['og_block_subscribers_is_admin']));
        }
        if (isset($edit['og_block_cnt'])) {
          variable_set("og_block_cnt_{$delta}", $edit['og_block_cnt']);
        }
        break;
      case 3:
        if (isset($edit['og_block_cnt'])) {
          variable_set("og_block_cnt_{$delta}", $edit['og_block_cnt']);
        }
        break;
    }
  }
}
function og_block_notifications() {
  global $user;
  if ($groupnode = og_get_group_context()) {

    // only members can see this block
    if (in_array($groupnode->nid, array_keys($user->og_groups))) {
      $content = t('This group offers a !groupfeed and a !subscription.', array(
        '!groupfeed' => l(t('RSS feed'), "node/{$groupnode->nid}/feed"),
        '!subscription' => l(t('subscription option'), 'og/manage/' . $groupnode->nid),
      ));
      if (module_exists('views') && module_exists('views_rss')) {

        // NOTE: See og.css for styling specific to these lists
        $content .= ' ' . t('Or subscribe to these personalized, sitewide feeds:');
        $inline = array(
          'class' => 'links inline',
        );
        $l1[] = array(
          'title' => t('Feed'),
          'href' => 'group/myunread/feed',
        );
        $l1[] = array(
          'title' => t('Page'),
          'href' => 'group/myunread',
        );
        $links['my_unread'] = t('My unread:') . ' ' . theme('links', $l1, $inline);
        $l2[] = array(
          'title' => t('Feed'),
          'href' => 'group/mytracker/feed',
        );
        $l2[] = array(
          'title' => t('Page'),
          'href' => 'group/mytracker',
        );
        $links['my_group'] = t('My group:') . ' ' . theme('links', $l2, $inline);
        $l3[] = array(
          'title' => t('Feed'),
          'href' => 'group/tracker/feed',
        );
        $l3[] = array(
          'title' => t('Page'),
          'href' => 'group/tracker',
        );
        $links['all_posts'] = t('All posts:') . ' ' . theme('links', $l3, $inline);
        $content .= theme('item_list', $links);
      }
      $block['content'] = $content;
      $block['subject'] = t('Group notifications');
      return $block;
    }
  }
}

/**
 * Return code that emits an XML icon. TODO: this belongs in theme.inc
 */
function theme_opml_icon($url) {
  if ($image = theme('image', drupal_get_path('module', 'og') . '/opml.gif', t('OPML file'), t('OPML file'))) {
    return '<a href="' . check_url($url) . '" class="opml-icon">' . $image . '</a>';
  }
}
function og_block_new() {
  list($types, $in) = og_get_sql_args();
  $sql = "SELECT COUNT(*) FROM {node} n INNER JOIN {og} og ON n.nid = og.nid WHERE og.directory=1 AND n.type {$in} AND n.status = 1";
  $cnt = db_result(db_query(db_rewrite_sql($sql), $types));
  if ($cnt > 0) {
    $max = variable_get('og_block_cnt_3', 10);
    $sql = "SELECT n.nid, n.title FROM {node} n INNER JOIN {og} og ON n.nid = og.nid WHERE n.status = 1 AND n.type {$in} AND og.directory=1 ORDER BY nid DESC";
    $result = db_query_range(db_rewrite_sql($sql), $types, 0, $max);
    $output = node_title_list($result);
    if ($cnt > $max) {
      $output .= '<div class="more-link">' . l(t('more'), 'og', array(
        'title' => t('Browse the newest groups.'),
      )) . '</div>';
    }
    $block['subject'] = t('New groups');
    $block['content'] = $output;
    return $block;
  }
}
function og_block_users_network() {
  global $user;
  if ($user->uid && count($user->og_groups)) {
    $max = variable_get('og_block_cnt_2', 10);
    $placeholders = array_fill(0, count($user->og_groups), "'%s'");
    $sql = "SELECT ogu.uid, u.name, u.picture FROM {og_uid} ogu INNER JOIN {users} u ON ogu.uid = u.uid WHERE ogu.uid != %d AND ogu.nid IN (" . implode(", ", $placeholders) . ") GROUP BY ogu.uid, u.name, u.picture ORDER BY u.name ASC";
    $args = array_keys($user->og_groups);
    array_unshift($args, $user->uid);
    $result = db_query_range($sql, $args, 0, $max);
    $block['content'] = og_user_title_list($result, NULL, FALSE);
    $block['subject'] = t('My Network');
    return $block;
  }
}
function og_block_subscribers() {
  global $user;
  if ($group_node = og_get_group_context()) {
    $gid = $group_node->nid;

    // only members can see membership list
    if (in_array($gid, array_keys($user->og_groups))) {
      $max = variable_get('og_block_cnt_2', 10);

      // Should we show admins, standard users, or both. From block configuration.
      $is_admin = variable_get("og_block_subscribers_is_admin", drupal_map_assoc(array(
        'members',
        'admins',
      )));
      $block['subject'] = t('Recently joined');
      $block['content'] = og_block_subscribers_list($gid, $max, $is_admin);
      return $block;
    }
  }
}

/**
 * API function for retrieving a list of group members. Used by og and og_panels
 * 
 * @return string
 *   An HTML list.
 **/
function og_block_subscribers_list($gid, $max = 10, $is_admin = array(
  'members',
  'admins',
), $show_more = TRUE, $show_picture = NULL) {
  $placeholders = array_fill(0, count(array_filter($is_admin)), '%d');
  $placeholders = implode(', ', $placeholders);
  $sql = "SELECT DISTINCT(u.uid), u.*, ogu.created FROM {og_uid} ogu INNER JOIN {users} u ON ogu.uid = u.uid WHERE ogu.is_admin IN ({$placeholders}) AND ogu.nid = %d AND ogu.is_active = 1 AND u.status = 1 ORDER BY ogu.created DESC";
  if (!empty($is_admin['members'])) {
    $args[] = 0;
  }
  if (!empty($is_admin['admins'])) {
    $args[] = 1;
  }
  $args[] = $gid;
  $result = db_query_range($sql, $args, 0, $max);
  return og_user_title_list($result, $gid, $show_more, $show_picture);
}

// shows users. analogous to node_title_list()
function og_user_title_list($result, $gid = NULL, $show_more = TRUE, $show_picture = NULL) {
  if (is_null($show_picture)) {
    $show_picture = og_is_picture();
  }
  while ($row = db_fetch_object($result)) {
    if ($show_picture) {

      //showing member pictures
      $link = theme('user_picture', $row) . theme('username', $row);
      $links[] = "\n" . '<div class="og-picture-wrapper">' . "\n" . $link . "\n" . '</div>' . "\n";
    }
    else {
      $link = theme('username', $row);
      $links[] = "\n" . $link . "\n";
    }
  }
  if ($links) {
    $max = variable_get('og_block_cnt_2', 10);
    if (count($links) > $max - 1) {
      array_pop($links);
      $txt = t('more');
      $title = array(
        'title' => t('View all members.'),
      );
      if ($show_more) {
        $more = $show_picture ? l($txt, "og/users/{$gid}/faces", $title) : l($txt, "og/users/{$gid}", $title);
        $append = "<div class=\"more-link\">{$more}</div>";
      }
    }
    return theme('item_list', $links) . $append;
  }
}
function og_block_details() {

  // only display group details if we have a group context
  if (($node = og_get_group_context()) && node_access('view', $node)) {

    // allow other og-enabled node types to provide their own block details
    $block = module_invoke($node->type, 'og_block_details', $node);
    if (!$block) {
      $block = og_og_block_details($node);
    }
    return $block;
  }
}

// todol make this a menu
function og_og_block_details($node) {
  global $user;
  list($txt, $subscription) = og_subscriber_count_link($node);
  if ($subscription == 'active' || user_access('administer nodes')) {
    $links = module_invoke_all('og_create_links', $node);

    // We want to open this up for OG_INVITE_ONLY but we need to handle invitation workflow much better. See http://drupal.org/node/170332
    if ($node->og_selective < OG_INVITE_ONLY) {
      $links['invite'] = l(t('Invite friend'), "og/invite/{$node->nid}");
    }
    $links['subscribers'] = $txt;
    $links['manager'] = t('Manager:') . ' ' . theme('username', $node);
    $subscribe = isset($subscription) ? l(t('My membership'), "og/manage/{$node->nid}") : og_subscribe_link($node);
    if (isset($subscribe)) {
      $links['my_membership'] = $subscribe;
    }
    if (module_exists('search') && user_access('search content')) {
      $post = drupal_get_form('og_search_form', $node);
    }
  }
  elseif ($subscription == 'requested') {
    $links['approval'] = t('Your membership request awaits approval.');
    $links['delete'] = l(t('delete request'), "og/unsubscribe/{$node->nid}", array(), 'destination=og');
  }
  elseif (!$user->uid) {
    $dest = drupal_get_destination();
    $links['must_login'] = t('You must <a href="!register">register</a>/<a href="!login">login</a> in order to post into this group.', array(
      '!register' => url("user/register", $dest),
      '!login' => url("user/login", $dest),
    ));
  }
  elseif ($node->og_selective < OG_INVITE_ONLY) {
    $links['subscribe'] = og_subscribe_link($node);
  }
  elseif ($node->og_selective == OG_INVITE_ONLY) {
    $links['closed'] = t('This is an @invite group. The group administrators add/remove members as needed.', array(
      '@invite' => t('invite only'),
    ));
  }
  elseif ($node->og_selective == OG_CLOSED) {
    $links['closed'] = t('This is a @closed group. The group administrators add/remove members as needed.', array(
      '@closed' => t('closed'),
    ));
  }

  // Modify these links by reference. If you want control of the whole block, see og_block_details().
  foreach (module_implements('og_link_alter') as $module) {
    $function = $module . '_og_link_alter';
    $function($links, $node);
  }
  $block['content'] = theme('item_list', $links) . $post;
  $block['subject'] = l($node->title, "node/{$node->nid}");
  return $block;
}

/**
 * Determine the number of active and pending members and the current user's membership state.
 * 
 * @return array
 *   An array containing two strings. One for the number of members and another containing 'active' or 'requested'
 */
function og_subscriber_count_link($node) {
  global $user;
  $result = db_query(og_list_users_sql(0), $node->nid);
  $cntall = db_num_rows($result);
  $cntpending = 0;
  while ($row = db_fetch_object($result)) {
    if ($row->is_active == 0) {
      $cntpending++;
    }
    if ($row->uid == $user->uid) {
      if ($row->is_active) {
        $subscription = 'active';
      }
      else {
        $subscription = 'requested';
      }
    }
  }
  $txt = format_plural($cntall - $cntpending, '1 member', '@count members');
  $txt = og_is_picture() ? l($txt, "og/users/{$node->nid}/faces") : l($txt, "og/users/{$node->nid}");
  $txt .= $cntpending ? " ({$cntpending})" : '';
  return array(
    $txt,
    $subscription,
  );
}
function og_subscribe_link($node) {
  if ($node->og_selective == OG_MODERATED) {
    $txt = t('Request membership');
  }
  elseif ($node->og_selective == OG_OPEN) {
    $txt = t('Join');
  }
  if (isset($txt)) {
    return l($txt, "og/subscribe/{$node->nid}", array(
      'rel' => 'nofollow',
    ), drupal_get_destination());
  }
}

// $group is an object containing the group node
function og_og_create_links($group) {
  $post_types = og_get_types('group_post');
  $types = node_get_types();
  foreach ($post_types as $post_type) {

    // We used to check for node_access(create) but then node admins would get a false positive and see node types that they could not create.
    // When this becomes a proper menu in D6, we get sorting for free
    $type_name = $types[$post_type]->name;
    if (module_invoke($types[$post_type]->module, 'access', 'create', $post_type)) {
      $links["create_{$post_type}"] = l(t('Create !type', array(
        '!type' => $type_name,
      )), "node/add/{$post_type}", array(
        'title' => t('Add a new !s in this group.', array(
          '!s' => $type_name,
        )),
      ), "gids[]={$group->nid}");
    }
  }
  return $links ? $links : array();
}
function og_search_form($group) {
  $form['filter0'] = array(
    '#type' => 'textfield',
    '#title' => '',
    '#description' => '',
    '#size' => 19,
    '#maxlength' => 255,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Search'),
  );
  $form['#process'] = array(
    'views_filters_process' => array(),
  );
  $form['#method'] = 'get';
  $form['#action'] = url("og/search/{$group->nid}");
  return $form;
}
function og_admin_settings() {
  drupal_set_title(t('Organic groups configuration'));

  // content types
  $is_configured = count(og_get_types('group')) && count(og_get_types('group_post'));
  if (!$is_configured) {
    form_set_error('content_types_table', t('You must designate at least one content type to act as a Group node and another as a Group post. <a href="!create">Create new content type</a> if needed.', array(
      '!create' => url('admin/content/types/add'),
    )));
  }
  $form['og_settings']['content_types'] = array(
    '#type' => 'fieldset',
    '#title' => t('Content types'),
    '#collapsible' => TRUE,
    '#collapsed' => $is_configured,
  );
  $header = array(
    t('Type'),
    t('Usage'),
    t('Operations'),
  );
  $map = og_types_map();
  foreach (node_get_types() as $type) {
    $type_url_str = str_replace('_', '-', $type->type);
    $usage = variable_get('og_content_type_usage_' . $type->type, 'omitted');
    $rows[] = array(
      $type->name,
      $map[$usage],
      l(t('Edit'), "admin/content/types/{$type_url_str}", array(), drupal_get_destination()),
    );
  }
  $form['og_settings']['content_types']['content_types_table'] = array(
    '#value' => theme('table', $header, $rows),
  );
  $form['og_settings']['group_details'] = array(
    '#type' => 'fieldset',
    '#title' => t('Group details'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );

  // groups directory visibility
  $options = array(
    t("New groups don't appear in the groups directory. Administrators control the directory exclusively."),
    t('New groups always appear in the groups directory.'),
    t('Group creator chooses whether her group appears in the directory. Defaults to %in.', array(
      '%in' => t('in directory'),
    )),
    t('Group creator chooses whether her group appears in the directory. Defaults to %out.', array(
      '%out' => t('not in directory'),
    )),
  );
  $form['og_settings']['group_details']['og_visibility_directory'] = array(
    '#type' => 'radios',
    '#title' => t('Groups directory control'),
    '#default_value' => variable_get('og_visibility_directory', OG_DIRECTORY_CHOOSE_TRUE),
    '#description' => t('OG admins always see the checkbox for adding a group to the %dir. Note that changing this setting has no effect on existing posts. Re-save those posts to acquire this new setting.', array(
      '%dir' => t('groups directory'),
    )),
    '#options' => $options,
  );

  // groups registration visibility
  $options = array(
    t("New groups don't appear on the registration form. Administrators control the form exclusively."),
    t('New groups always appear on the registration form.'),
    t('Group creator chooses whether her group appears on the registration form. Defaults to %in.', array(
      '%in' => t('on form'),
    )),
    t('Group creator chooses whether her group appears on the registration form. Defaults to %out.', array(
      '%out' => t('not on form'),
    )),
  );
  $form['og_settings']['group_details']['og_visibility_registration'] = array(
    '#type' => 'radios',
    '#title' => t('Registration form control'),
    '#default_value' => variable_get('og_visibility_registration', OG_REGISTRATION_CHOOSE_FALSE),
    '#description' => t('OG admins always see the checkbox for adding a group to the %dir. Note that changing this setting has no effect on existing posts. Re-save those posts to acquire this new setting.', array(
      '%dir' => t('registration form'),
    )),
    '#options' => $options,
  );

  // audience checkboxes
  $form['og_settings']['group_details']['og_audience_checkboxes'] = array(
    '#type' => 'checkbox',
    '#title' => t('Audience checkboxes'),
    '#default_value' => variable_get('og_audience_checkboxes', TRUE),
    '#description' => t('Show each group that the user is a member of as a checkbox in the Audience section. This enables the member to place her post into multiple groups. If unchecked, simplify the user interface by omitting the checkboxes and assuming user wants to post into the current group. This simplification only applies to new nodes, and not to edits of existing nodes. Group administrators always see checkboxes.'),
  );

  // audience required
  $options = array(
    t('optional'),
    t('required'),
  );
  $form['og_settings']['group_details']['og_audience_required'] = array(
    '#type' => 'radios',
    '#title' => t('Audience required'),
    '#default_value' => variable_get('og_audience_required', 0),
    '#options' => $options,
    '#description' => t('Do you require that all (non administrator) posts be affiliated with a group? Note that changing this setting will affect existing posts when they are edited.'),
  );
  unset($options);

  // audience home page - Views
  $options = og_get_available_views();
  $form['og_settings']['group_details']['og_home_page_view'] = array(
    '#type' => 'radios',
    '#title' => t('Home page presentation'),
    '#options' => $options,
    '#default_value' => variable_get('og_home_page_view', 'og_ghp_ron'),
    '#description' => t('Pick a View for your group home page. Only Views whose names start with <strong>og_ghp_</strong> are eligible. The View determines the layout of your group home page. You may alter the presentation using !theme. Also see the Theme section of the !README. Also note that group admins can override this presentation using the included <em>Organic Groups Panels</em> module.', array(
      '!README' => og_readme(),
      '!theme' => l(t('typical Views themeing techniques'), 'http://drupal.org/node/42597'),
    )),
  );

  // member pictures
  $form['og_settings']['group_details']['og_member_pics'] = array(
    '#type' => 'checkbox',
    '#title' => t('Member pictures'),
    '#default_value' => variable_get('og_member_pics', TRUE),
    '#description' => t('Should member pictures be shown on the members page, the group members block, and group details block? You must also enable pictures in !user.', array(
      '!user' => l(t('User configuration'), 'admin/user/settings'),
    )),
  );

  // Messages fieldset.
  $form['og_settings']['notifications'] = array(
    '#type' => 'fieldset',
    '#title' => t('Messaging & Notifications'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['og_settings']['notifications']['og_email_notification_pattern'] = array(
    '#type' => 'textfield',
    '#title' => t('Format of From: field'),
    '#default_value' => variable_get("og_email_notification_pattern", '@user_name  <@site_mail>'),
    '#description' => t('Specify the format of the "From:" field on outgoing notifications. Available variables: @user_mail, @user_name, @site_mail, @site_name. Note that the @user_mail token reveals the author\'s email address. If the admin email examples above appear blank, you need to set your site email in the Site Configuration panel.'),
  );
  $form['og_settings']['notifications']['og_new_node_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('New content subject'),
    '#description' => t('Subject of notification message for new content. Available variables: @group, !group_url, @type, @site, !content_url, !reply_url, @title, @subject, @node_full, @node_teaser, @username. %subject contains the comment title in the case of a comment but the node title in the case of a new post. @title is always the node title.'),
    '#default_value' => _og_user_mail_text('og_new_node_subject'),
  );
  $form['og_settings']['notifications']['og_new_node_body'] = array(
    '#type' => 'textarea',
    '#title' => t('New content body'),
    '#rows' => 10,
    '#description' => t('Body of the notification for new content. Available variables: @group, !group_url, @type, @site, !content_url, !reply_url, @title, @subject, @node_full, @node_teaser, @username. @subject contains the comment title in the case of a comment but the node title in the case of a new post. %title is always the node title.'),
    '#default_value' => _og_user_mail_text('og_new_node_body'),
  );
  $form['og_settings']['notifications']['og_admin_email_body'] = array(
    '#type' => 'textarea',
    '#title' => t('Group admin notification body'),
    '#rows' => 10,
    '#description' => t('The body of the message sent to users from the group admin. Available variables: @group, @body, @site, !url_group, !url_unsubscribe'),
    '#default_value' => _og_user_mail_text('og_admin_email_body'),
  );
  $form['og_settings']['notifications']['og_approve_user_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('User approved notification subject'),
    '#description' => t('The subject of the message sent to new approved members. Available variables: !group_url, @title'),
    '#default_value' => _og_user_mail_text('og_approve_user_subject'),
  );
  $form['og_settings']['notifications']['og_approve_user_body'] = array(
    '#type' => 'textarea',
    '#title' => t('User approved notification body'),
    '#rows' => 10,
    '#description' => t('The body of the message sent to new approved members. Available variables: !group_url, @title'),
    '#default_value' => _og_user_mail_text('og_approve_user_body'),
  );
  $form['og_settings']['notifications']['og_deny_user_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('User denied notification subject'),
    '#description' => t('The subject of the message sent to denied users. Available variables: !group_url, @title'),
    '#default_value' => _og_user_mail_text('og_deny_user_subject'),
  );
  $form['og_settings']['notifications']['og_deny_user_body'] = array(
    '#type' => 'textarea',
    '#title' => t('User denied notification body'),
    '#rows' => 10,
    '#description' => t('The body of the message sent to denied users. Available variables: !group_url, @title'),
    '#default_value' => _og_user_mail_text('og_deny_user_body'),
  );
  $form['og_settings']['notifications']['og_invite_user_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Invite user notification subject'),
    '#description' => t('The subject of the message sent to users invited to join a group. Available variables: @group, @site, @description, !group_url, @body'),
    '#default_value' => _og_user_mail_text('og_invite_user_subject'),
  );
  $form['og_settings']['notifications']['og_invite_user_body'] = array(
    '#type' => 'textarea',
    '#title' => t('Invite user notification body'),
    '#rows' => 10,
    '#description' => t('The body of the message sent to users invited to join a group. Available variables: @group, @site, @description, !group_url, @body'),
    '#default_value' => _og_user_mail_text('og_invite_user_body'),
  );
  $form['og_settings']['notifications']['og_request_user_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Request user notification subject'),
    '#description' => t("The subject of the message sent to a user's request to join a group. Available variables: @group, @username, !approve_url, !group_url"),
    '#default_value' => _og_user_mail_text('og_request_user_subject'),
  );
  $form['og_settings']['notifications']['og_request_user_body'] = array(
    '#type' => 'textarea',
    '#title' => t('Request user notification body'),
    '#rows' => 10,
    '#description' => t("The body of the message sent to a user's request to join a group. Available variables: @group, @username, !approve_url, !group_url"),
    '#default_value' => _og_user_mail_text('og_request_user_body'),
  );
  $form['og_settings']['notifications']['og_new_admin_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('New admin user notification subject'),
    '#description' => t('The subject of the message sent to a new admin for a group. Available variables: @group, @username, !group_url'),
    '#default_value' => _og_user_mail_text('og_new_admin_subject'),
  );
  $form['og_settings']['notifications']['og_new_admin_body'] = array(
    '#type' => 'textarea',
    '#title' => t('New admin user notification body'),
    '#rows' => 10,
    '#description' => t('The body of the message sent to a new admin for a group. Available variables: @group, @username, !group_url, !invite_url'),
    '#default_value' => _og_user_mail_text('og_new_admin_body'),
  );
  return system_settings_form($form);
}
function og_is_picture() {
  return variable_get('user_pictures', 0) && variable_get('og_member_pics', TRUE);
}

// TODO: move this to new API function in Views where I can influence the WHERE clause.
function og_get_available_views() {
  $views = array();
  $result = db_query("SELECT name, description FROM {view_view} WHERE name LIKE 'og_ghp_%'");
  while ($view = db_fetch_object($result)) {
    $views[$view->name] = check_plain("{$view->description} ({$view->name})");
  }
  views_load_cache();

  // the function below was not loaded without this call
  $default_views = _views_get_default_views();
  $views_status = variable_get('views_defaults', array());
  foreach ($default_views as $view) {

    // filter this list to simplify for admins.
    if (substr($view->name, 0, 7) == 'og_ghp_') {
      if (!$views[$view->name] && ($views_status[$view->name] == 'enabled' || !$views_status[$view->name] && !$view->disabled)) {
        $views[$view->name] = check_plain("{$view->description} ({$view->name})");
      }
    }
  }
  ksort($views);
  return $views;
}

// TODO: maybe use a custom theme('mark') here?
function theme_og_format_subscriber_status($group) {
  if (!$group['is_active']) {
    return ' ' . t('(pending approval)');
  }
}

/**
 * Implementation of hook_xmlrpc().
 */
function og_xmlrpc() {
  require_once drupal_get_path('module', 'og') . '/og_xmlrpc.inc';
  return array(
    array(
      'og.subscribe_user',
      'og_xmlrpc_subscribe_user',
      array(
        'struct',
        'string',
        'string',
        'int',
        'int',
      ),
      t('Add a user to a group.'),
    ),
    array(
      'og.getAllSubscribers',
      'og_xmlrpc_get_all_subscribers',
      array(
        'array',
        'string',
        'string',
        'int',
        'int',
        'int',
      ),
      t('All members for a given group.'),
    ),
    array(
      'og.getUserGroups',
      'og_xmlrpc_get_user_groups',
      array(
        'array',
        'string',
        'string',
        'int',
      ),
      t('Retrieve the group memberships for a given user.'),
    ),
  );
}

/*
* Implementation of hook_pathauto_node(). Deprecated by pathauto, which now uses tokens. Will be removed one day.
*/
function og_pathauto_node($op, $node = NULL) {
  switch ($op) {
    case 'placeholders':
      $placeholders = array();
      $placeholders[t('[ogname]')] = t('The name of the organic group this post belongs to.');
      return $placeholders;
    case 'values':
      $results = array();
      if ($node->og_groups) {
        foreach ($node->og_groups as $gid) {
          if ($gid != 0) {
            $name = db_result(db_query("SELECT title FROM {node} WHERE nid = %d", $gid));
            break;
          }
        }
      }
      $results[t('[ogname]')] = pathauto_cleanstring($name);
      return $results;
    default:
      break;
  }
}

/**
 * Implementation of hook_token_list() for og specific tokens
 */
function og_token_list($type = 'all') {
  if ($type == 'node' || $type == 'all') {
    $tokens['node']['ogname'] = t("Title of top group");
    $tokens['node']['ogname-raw'] = t("Unfiltered title of top group. WARNING - raw user input.");
    $tokens['node']['og-id'] = t("ID of top group");
    return $tokens;
  }
}

/**
 * Implementation of hook_token_values() for og specific tokens
 */
function og_token_values($type, $object = NULL) {
  switch ($type) {
    case 'node':

      // Set some defaults.
      $values['ogname'] = '';
      $values['ogname-raw'] = '';
      $values['og-id'] = '';
      if (is_array($object->og_groups)) {
        $gids = array_filter($object->og_groups);
        foreach ($gids as $gid) {
          $title = db_result(db_query("SELECT title FROM {node} WHERE nid = %d", $gid));
          $values['ogname'] = check_plain($title);
          $values['ogname-raw'] = $title;
          $values['og-id'] = $gid;
          break;
        }
      }
      return $values;
  }
  return;
}

/**
 * An implementation of hook_db_rewrite_sql(). Used by other modules to filter nodes to a given group
 *
 * You must pass $args with element 'og_nid' => $nid in order to have your query filtered by this function
 *
 * @return array
 */
function og_db_rewrite_sql($sql, $primary_table, $primary_field, $args) {
  if (isset($args['og_nid']) && is_numeric($args['og_nid'])) {
    $query['join'] = 'INNER JOIN {og_ancestry} oga ON n.nid = oga.nid';
    $query['where'] = 'oga.group_nid = ' . $args['og_nid'];
    return $query;
  }
}
function og_requirements($phase) {
  $requirements = array();

  // Ensure translations don't break at install time
  $t = get_t();
  if ($phase == 'runtime') {
    $og_types = og_get_types('group');
    $all_types = array_keys(node_get_types('types'));
    if (!count(array_intersect($og_types, $all_types))) {
      $requirements['og_group_types'] = array(
        'title' => $t('Organic groups group type'),
        'value' => $t('You have no node types which are acting as groups. See the Notes section of the !readme and the Content types fieldset at top of <a href="!settings">OG settings</a>.', array(
          '!readme' => og_readme(),
          '!settings' => url('admin/og/og'),
        )),
        'severity' => REQUIREMENT_ERROR,
      );
    }
    if (!module_exists('og_access')) {
      $requirements['og_access'] = array(
        'title' => $t('Organic groups access control'),
        'value' => $t('Organic groups access control module is disabled. See the !modules', array(
          '!modules' => l($t('modules page'), 'admin/build/modules'),
        )),
        'severity' => REQUIREMENT_INFO,
      );
    }
  }
  return $requirements;
}
function og_readme() {
  global $base_path;

  // this link has to work when clean urls are disabled and drupal in subdir.
  $href = drupal_get_path('module', 'og') . '/README.txt';
  $link = "<a href=\"{$base_path}{$href}\">" . t('README file') . '</a>';
  return $link;
}

/**
 * Handle '$group' in a URL.
 */
function og_url_group($token, $argument, $arg) {
  global $user;
  if (!is_numeric($arg)) {
    return FALSE;
  }
  $node = node_load($arg);
  if (og_is_group_type($node->type)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Access callback: og_notifications (or similar) is required for the broadcast
 * tab. Override menu item to amend.
 */
function og_broadcast_access($node) {
  return og_is_node_admin($node) && module_exists('og_notifications');
}

Functions

Namesort descending Description
og_add_users
og_add_users_submit
og_add_users_validate
og_admin_settings
og_all_groups_options
og_approve
og_block Implementation of hook_block().
og_block_details
og_block_new
og_block_notifications
og_block_subscribers
og_block_subscribers_list API function for retrieving a list of group members. Used by og and og_panels
og_block_users_network
og_broadcast_access Access callback: og_notifications (or similar) is required for the broadcast tab. Override menu item to amend.
og_broadcast_form Admins may broadcast messages to all their members.
og_broadcast_form_submit
og_confirm_subscribe Confirm og membership form
og_confirm_subscribe_submit Confirm og membership submit handler
og_confirm_unsubscribe Confirm og unsubscription form
og_confirm_unsubscribe_submit Confirm og unsubscription submit handler
og_create_admin
og_create_admin_confirm OG create admin form
og_create_admin_confirm_submit Confirm og create admin form
og_db_rewrite_sql An implementation of hook_db_rewrite_sql(). Used by other modules to filter nodes to a given group
og_delete_admin
og_delete_subscription
og_deny
og_feed Menu callback. Handle old feed urls with permanent redirect. Hopefully rss readers respect that. Don't bother with the overhead of drupal_goto()
og_form_add_og_audience Helper method to add OG audience fields to a given form. This is lives in a separate function from og_form_alter() so it can be shared by other OG contrib modules.
og_form_alter
og_get_available_views
og_get_group_context API function for getting the group context (if any) for the current request. Used for things like setting current theme and breadcrumbs. This context is set during og_theme()
og_get_node_groups
og_get_node_groups_result
og_get_sql_args
og_get_subscriptions
og_get_types
og_group_child_nids
og_group_form Adds standard fields for any node configured to be a group node
og_help
og_home_empty
og_init This processing cannot happen later in the request because
og_insert_group
og_invite_form
og_invite_form_submit
og_invite_form_validate
og_invite_page
og_is_group_member Check a user's membership in a group. Site admins always return TRUE.
og_is_group_post_type
og_is_group_type API function for determining whether a given node type has been designated by admin to behave as a group node (i.e. a container).
og_is_node_admin
og_is_omitted_type
og_is_picture
og_is_wiki_type
og_list_users_faces_page
og_list_users_page
og_list_users_sql
og_load_group
og_manage
og_manage_form
og_manage_form_submit
og_menu
og_menu_check
og_menu_check_node_view
og_msgid_server
og_nodeapi Implementation of hook_nodeapi().
og_node_delete_confirm_submit
og_node_delete_group_form
og_node_delete_nongroup_form
og_node_groups_distinguish Iterate over a set of groups and separate out those that are inaccessible to the current user. Don't return groups of which the current user is a member, unless $exclude_joined=FALSE is passed. Used in og_form_add_og_audience() and node.tpl.php.
og_node_type An implementation of hook_node_type. Automatically update admin preferences when node type is renamed or removed.
og_node_type_form_submit
og_node_view Similar to node_view() but without calling theme('node').
og_og_block_details
og_og_create_links
og_opml
og_page_activity
og_pathauto_node
og_perm Implementation of hook_perm().
og_readme
og_remove_admin_confirm OG remove admin form
og_remove_admin_confirm_submit Confirm og remove admin form
og_requirements
og_save_ancestry
og_save_subscription Low level function for managing membership
og_search_form
og_set_group_context
og_set_locale Like locale_initialize(), but includes a check for group language and sets accordingly. Priority goes: user => group => site default
og_set_theme
og_submit_group
og_subscribe
og_subscriber_count_link Determine the number of active and pending members and the current user's membership state.
og_subscribe_link
og_subscribe_user Create a new membership for a given user to given group. Edits to membership should go through og_save_subscription(). No access control since this is an API function.
og_theme Override theme based on what group is being displayed (if any). Be smart about selecting the 'active' group for ambigous urls like node/$nid
og_token_list Implementation of hook_token_list() for og specific tokens
og_token_values Implementation of hook_token_values() for og specific tokens
og_types_map
og_unsubscribe
og_update_group
og_url_group Handle '$group' in a URL.
og_user
og_user_title_list
og_view_group
og_xmlrpc Implementation of hook_xmlrpc().
theme_og_format_subscriber_status
theme_og_mission
theme_og_picture_grid
theme_opml_icon Return code that emits an XML icon. TODO: this belongs in theme.inc
_og_user_mail_text Define all OG message strings. Modelled after Drupal's user.module

Constants