<?php


/**
 * @file
 * Enable access control for private and public groups and group content.
 */

/**
 * The access realm of group member.
 */
define('OG_ACCESS_REALM', 'og_access');

/**
 * Group public access field.
 */
define('OG_ACCESS_FIELD', 'group_access');

/**
 * Group public access field.
 */
define('OG_CONTENT_ACCESS_FIELD', 'group_content_access');

/**
 * Define group content access by it's group defaults.
 */
define('OG_CONTENT_ACCESS_DEFAULT', 0);

/**
 * Define group content access public regardless of its group definition.
 */
define('OG_CONTENT_ACCESS_PUBLIC', 1);

/**
 * Define group content access private regardless of its group definition.
 */
define('OG_CONTENT_ACCESS_PRIVATE', 2);

/**
 * The default variable name that controls abtch processing on
 * group privacy change.
 */
define('OG_ACCESS_PRIVACY_CHANGE_BATCH_PROCESSING', 'og_access_privacy_change_batch_processing');

/**
 * Implements hook_node_grants().
 */
function og_access_node_grants($account, $op) {
  if ($op != 'view') {
    return;
  }

  if ($groups = og_get_entity_groups('user', $account)) {
    foreach ($groups as $group_type => $gids) {
      foreach ($gids as $gid) {
        $realm = OG_ACCESS_REALM . ':' . $group_type;
        $grants[$realm][] = $gid;
      }
    }
  }

  return !empty($grants) ? $grants : array();
}


/**
 * Implements hook_node_access_records().
 */
function og_access_node_access_records($node) {
  if (empty($node->status)) {
    // Node is unpublished, so we don't allow every group member to see
    // it.
    return array();
  }

  // The group IDs, that in case access is granted, will be recorded.
  $gids = array();

  $wrapper = entity_metadata_wrapper('node', $node);

  // Verify that a group content with visibility field can't create when there
  // isn't an OG access field attached to the group entity.
  if (!empty($wrapper->{OG_CONTENT_ACCESS_FIELD}) && $wrapper->{OG_CONTENT_ACCESS_FIELD}->value() == OG_CONTENT_ACCESS_DEFAULT) {
    _og_access_verify_access_field_existence($node);
  }

  if (!empty($wrapper->{OG_ACCESS_FIELD}) && $wrapper->{OG_ACCESS_FIELD}->value() && og_is_group('node', $node)) {
    // Private group.
    $gids['node'][] = $node->nid;
  }
  // If there is no content access field on the group content, we assume
  // that the group defaults are needed.
  // This allows us not to have the content access field on the group
  // content but still have access control.
  $content_access = !empty($wrapper->{OG_CONTENT_ACCESS_FIELD}) ? $wrapper->{OG_CONTENT_ACCESS_FIELD}->value() : OG_CONTENT_ACCESS_DEFAULT;
  switch ($content_access) {
    case OG_CONTENT_ACCESS_DEFAULT:
      if (!$entity_groups = og_get_entity_groups('node', $node)) {
        break;
      }

      $has_private = FALSE;
      foreach ($entity_groups as $group_type => $values) {
        entity_load($group_type, $values);
        foreach ($values as $gid) {
          $list_gids[$group_type][] = $gid;
          if ($has_private) {
            // We already know we have a private group, so we can avoid
            // re-checking it.
            continue;
          }
          $group_wrapper = entity_metadata_wrapper($group_type, $gid);
          if (!empty($group_wrapper->{OG_ACCESS_FIELD}) && $group_wrapper->{OG_ACCESS_FIELD}->value()) {
            $has_private = TRUE;
          }
        }
      }

      if ($has_private) {
        $gids = array_merge_recursive($gids, $list_gids);
      }
      break;

    case OG_CONTENT_ACCESS_PUBLIC:
      // Do nothing.
      break;

    case OG_CONTENT_ACCESS_PRIVATE:
      $gids = array_merge_recursive($gids, og_get_entity_groups('node', $node));
      break;
  }

  foreach ($gids as $group_type => $values) {
    foreach ($values as $gid) {
      $grants[] = array (
        'realm' => OG_ACCESS_REALM . ':' . $group_type,
        'gid' => $gid,
        'grant_view' => 1,
        'grant_update' => 0,
        'grant_delete' => 0,
        'priority' => 0,
      );
    }
  }

  return !empty($grants) ? $grants : array();
}

/**
 * Implement hook_og_fields_info().
 */
function og_access_og_fields_info() {
  $allowed_values = array(
    0 => 'Public - accessible to all site users',
    1 =>  'Private - accessible only to group members',
  );
  $items[OG_ACCESS_FIELD] = array(
    'type' => array('group'),
    'description' => t('Determine access to the group.'),
    // Private access can be done only on node entity.
    'entity' => array('node'),
    'field' => array(
      'field_name' => OG_ACCESS_FIELD,
      'no_ui' => TRUE,
      'type' => 'list_boolean',
      'cardinality' => 1,
      'settings' => array('allowed_values' => $allowed_values, 'allowed_values_function' => ''),
    ),
    'instance' => array(
      'label' => t('Group visibility'),
      'required' => TRUE,
      // Default to public.
      'default_value' => array(0 => array('value' => 0)),
      'widget_type' => 'options_select',
      'view modes' => array(
        'full' => array(
          'label' => 'above',
          'type' => 'options_onoff',
        ),
        'teaser' => array(
          'label' => 'above',
          'type' => 'options_onoff',
        ),
      ),
    ),
  );

  $allowed_values = array(
    0 => 'Use group defaults',
    1 => 'Public - accessible to all site users',
    2 => 'Private - accessible only to group members',

  );
  $items[OG_CONTENT_ACCESS_FIELD] = array(
    'type' => array('group content'),
    'description' => t('Determine access to the group content, which may override the group settings.'),
    // Private access can be done only on node entity.
    'entity' => array('node'),
    'field' => array(
      'field_name' => OG_CONTENT_ACCESS_FIELD,
      'no_ui' => TRUE,
      'type' => 'list_integer',
      'cardinality' => 1,
      'settings' => array('allowed_values' => $allowed_values, 'allowed_values_function' => ''),
    ),
    'instance' => array(
      'label' => t('Group content visibility'),
      'required' => TRUE,
      // Make the group type default.
      'default_value' => array(0 => array('value' => 0)),
      'widget_type' => 'options_select',
      'view modes' => array(
        'full' => array(
          'label' => 'above',
          'type' => 'list_default',
        ),
        'teaser' => array(
          'label' => 'above',
          'type' => 'list_default',
        ),
      ),
    ),
  );

  return $items;
}

/**
 * Verify that the OG field access is attached to the group.
 *
 * @param $node
 *  The node object.
 *
 * @throws OgException
 *  When the OG access field isn't attached to the group's node but the
 *  visibility field attached to the node.
 */
function _og_access_verify_access_field_existence($node) {
  // Verify that the OG access field is attached to the group(s) content.
  $fields_names = og_get_group_audience_fields('node', $node->type);

  $groups_bundles = og_get_all_group_bundle();

  // Check each group audience field on this node type.
  foreach (array_keys($fields_names) as $field_name) {
    // Get the group entity type that this field points to.
    $field_info = field_info_field($field_name);
    $target_type = $field_info['settings']['target_type'];

    foreach (array_keys($groups_bundles[$target_type]) as $bundle) {
      $instances = field_info_instances($target_type, $bundle);
      // Verify that the OG access field is attached to the group bundles.
      if (empty($instances[OG_ACCESS_FIELD])) {
        $params = array(
          '!nid' => $node->nid,
          '%entity_type' => $target_type,
          '%bundle' => $bundle,
        );
        throw new OgException(format_string('Cannot set visibility of node ID !nid as the %entity_type group of type %bundle does not have the "Group visibility" field attached to it.', $params));
      }
    }
  }
}

/**
 * Implements hook_og_access_invoke_node_access_acquire_grants().
 *
 * Simple check whether OG_ACCESS_FIELD values was changed.
 */
function og_access_og_access_invoke_node_access_acquire_grants($context) {
  $wrapper = entity_metadata_wrapper($context['entity_type'], $context['entity']);
  if (!isset($wrapper->{OG_ACCESS_FIELD})) {
    // Group doesn't have OG access field attached to it.
    return;
  }

  $original_wrapper = entity_metadata_wrapper($context['entity_type'], $context['entity']->original);

  $og_access = $wrapper->{OG_ACCESS_FIELD}->value();
  $original_og_access = $original_wrapper->{OG_ACCESS_FIELD}->value();

  return $og_access !== $original_og_access;
}

/**
 * Check whether group access changed that require batch processing.
 *
 * Note that batch processing is possible only when the event is triggered from
 * the UI.
 *
 * @param $entity
 *  The group entity object.
 * @param $entity_type
 *   The group type.
 *
 * @return bool
 *  TRUE if content permissions of group content should be rebuilt.
 */
function og_access_check_node_access_grants_is_needed($entity, $entity_type) {
  // Check whether group privacy change batch processing is needed.
  if (!variable_get(OG_ACCESS_PRIVACY_CHANGE_BATCH_PROCESSING, TRUE)) {
    return FALSE;
  }

  // Check if entity is the group.
  if (!og_is_group($entity_type, $entity)) {
    return FALSE;
  }

  // Check for access change.
  $context = array(
    'entity' => $entity,
    'entity_type' => $entity_type,
  );
  $result = module_invoke_all('og_access_invoke_node_access_acquire_grants', $context);

  drupal_alter('og_access_invoke_node_access_acquire_grants', $result, $context);

  return in_array(TRUE, $result);
}

/**
 * Implements hook_entity_update().
 *
 * In case the group entity was marked with group privacy change,
 * create batch operation to update group content.
 */
function og_access_entity_update($entity, $entity_type) {
  if (!og_access_check_node_access_grants_is_needed($entity, $entity_type)) {
    return;
  }

  // Handle group privacy change.
  og_access_handle_group_privacy_change($entity_type, $entity);
}

/**
 * In case of group privacy change, create batch operation to update group content.
 *
 * When group privacy (OG_ACCESS_FIELD) is changed, its group content should be
 * changed as well. Since a group might have big amount of content, it is being
 * handled using Batch API.
 *
 *
 * @param $entity_type
 *   The group type.
 * @param $entity
 *   The group entity object.
 */
function og_access_handle_group_privacy_change($entity_type, $entity) {
  list($id) = entity_extract_ids($entity_type, $entity);
  $batch = array(
    'title' => t('Handle group privacy change'),
    'operations' => array(
      array('og_access_invoke_node_access_acquire_grants', array($entity_type, $id)),
    ),
  );
  batch_set($batch);
}

/**
 * Batch API group content privacy change callback.
 *
 * @param $group_type
 *   The group type to handle.
 * @param $group_id
 *   The group id to handle.
 * @param $context
 *   Batch API context.
 */
function og_access_invoke_node_access_acquire_grants($group_type, $group_id, &$context) {
  if (empty($context['sandbox'])) {
    // Count relevant nodes.
    $query = new EntityFieldQuery();
    $total = $query
      ->entityCondition('entity_type', 'og_membership')
      ->propertyCondition('group_type', $group_type, '=')
      ->propertyCondition('entity_type', 'node', '=')
      ->propertyCondition('gid', $group_id, '=')
      ->count()->execute();

    $context['sandbox']['progress'] = 0;
    $context['sandbox']['last_id'] = 0;
    $context['sandbox']['total'] = $total;
  }

  $limit = 50;

  // Retrieve the next batch.
  $query = new EntityFieldQuery();
  $result = $query
    ->entityCondition('entity_type', 'og_membership')
    ->propertyCondition('group_type', $group_type, '=')
    ->propertyCondition('entity_type', 'node', '=')
    ->range(0, $limit)
    ->propertyCondition('etid', $context['sandbox']['last_id'], '>')
    ->propertyCondition('gid', $group_id, '=')
    // Order by the entity ID as the memberships are not ordered by this by default.
    ->propertyOrderBy('etid', 'ASC')
    ->execute();

  // Mark "finished" if there are no more results.
  if (!isset($result['og_membership'])) {
    $context['finished'] = 1;
    return;
  }

  foreach (entity_load('og_membership', array_keys($result['og_membership'])) as $og_membership) {
    // Load the node group content and "refresh" its grants.
    $node = node_load($og_membership->etid);

    // Rebuild the permissions for the node.
    node_access_acquire_grants($node);
    $context['sandbox']['progress']++;
    $context['sandbox']['last_id'] = $node->nid;
  }
  $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['total'];
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function og_access_form_og_ui_admin_settings_alter(&$form, &$form_state) {
  $form[OG_ACCESS_PRIVACY_CHANGE_BATCH_PROCESSING] = array(
    '#type' => 'checkbox',
    '#title' => t('Update group content privacy'),
    '#description' => t('Upon group privacy change, create batch operation to update group content.'),
    '#default_value' => variable_get(OG_ACCESS_PRIVACY_CHANGE_BATCH_PROCESSING, TRUE),
  );
}
