class flag_flag in Flag 7.3
Same name and namespace in other branches
- 5 flag.inc \flag_flag
- 6.2 flag.inc \flag_flag
- 6 flag.inc \flag_flag
- 7.2 flag.inc \flag_flag
This abstract class represents a flag, or, in Views 2 terminology, "a handler".
This is the base class for all flag implementations. Notable derived classes are flag_node and flag_comment.
There are several ways to obtain a flag handler, operating at different levels.
To load an existing flag that's defined in database or code, use one of:
- flag_get_flag(), the main flag API function.
- flag_load(), the loader for hook_menu().
- flag_get_flags(), the main API function for loading all flags. This calls flag_get_default_flags() to get flags in code.
The above all use factory methods to instantiate the object for the flag and load in its settings from configuration. The factory methods are:
- flag_flag::factory_by_row(), creates a flag handler from a database row. This is used by all the API functions above.
- flag_flag::factory_by_array(), creates a flag handler from a configuration array. This is used by flag_get_default_flags() and the flag import form.
- flag_flag::factory_by_entity_type(), creates an empty flag handler for the given entity type. This is used when a new or dummy flag handler is required and there is no configuration yet.
The factory methods in turn all call the low-level function flag_create_handler(), which obtains the correct handler for the flag, or if that can't be found, the special handler flag_broken. Finally, this calls $flag->construct() on the new handler object.
Hierarchy
- class \flag_flag
Expanded class hierarchy of flag_flag
1 string reference to 'flag_flag'
- flag_action_info_alter in includes/
flag.actions.inc - Implements hook_action_info_alter().
File
- includes/
flag/ flag_flag.inc, line 41 - Contains the flag_flag class. Flag type classes use an object oriented style inspired by that of Views 2.
View source
class flag_flag {
/**
* The database ID.
*
* NULL for flags that haven't been saved to the database yet.
*
* @var integer
*/
var $fid = NULL;
/**
* The entity type this flag works with.
*
* @var string
*/
var $entity_type = NULL;
/**
* The flag's "machine readable" name.
*
* @var string
*/
var $name = '';
/**
* The human-readable title for this flag.
*
* @var string
*/
var $title = '';
/**
* Whether this flag state should act as a single toggle to all users.
*
* @var bool
*/
var $global = FALSE;
/**
* The sub-types, AKA bundles, this flag applies to.
*
* This may be an empty array to indicate all types apply.
*
* @var array
*/
var $types = array();
/**
* The roles array. This can be populated by fetch_roles() when needed.
*/
var $roles = array(
'flag' => array(),
'unflag' => array(),
);
/**
* An associative array containing textual errors that may be created during
* validation.
*
* The array keys should reflect the type of error being set. At this time,
* the only "special" behavior related to the array keys is that
* drupal_access_denied() is called when the key is 'access-denied' and
* javascript is disabled.
*
* @var array
*/
public $errors = array();
/**
* Creates a flag from a database row. Returns it.
*
* This is static method.
*
* The reason this isn't a non-static instance method --like Views's init()--
* is because the class to instantiate changes according to the 'entity_type'
* database column. This design pattern is known as the "Single Table
* Inheritance".
*/
static function factory_by_row($row) {
$flag = flag_create_handler($row->entity_type);
// Lump all data unto the object...
foreach ($row as $field => $value) {
$flag->{$field} = $value;
}
// ...but skip the following two.
unset($flag->options, $flag->type);
// Populate the options with the defaults.
$options = (array) unserialize($row->options);
$options += $flag
->options();
// Make the unserialized options accessible as normal properties.
foreach ($options as $option => $value) {
$flag->{$option} = $value;
}
if (!empty($row->type)) {
// The loop loading from the database should further populate this
// property.
$flag->types[] = $row->type;
}
return $flag;
}
/**
* Create a complete flag (except an FID) from an array definition.
*/
static function factory_by_array($config) {
// Allow for flags with a missing entity type.
$config += array(
'entity_type' => FALSE,
);
$flag = flag_create_handler($config['entity_type']);
foreach ($config as $option => $value) {
$flag->{$option} = $value;
}
if (isset($config['locked']) && is_array($config['locked'])) {
$flag->locked = drupal_map_assoc($config['locked']);
}
return $flag;
}
/**
* Another factory method. Returns a new, "empty" flag; e.g., one suitable for
* the "Add new flag" page.
*/
static function factory_by_entity_type($entity_type) {
return flag_create_handler($entity_type);
}
/**
* Declares the options this flag supports, and their default values.
*
* Derived classes should want to override this.
*/
function options() {
$options = array(
// The text for the "flag this" link for this flag.
'flag_short' => '',
// The description of the "flag this" link.
'flag_long' => '',
// Message displayed after flagging an entity.
'flag_message' => '',
// Likewise but for unflagged.
'unflag_short' => '',
'unflag_long' => '',
'unflag_message' => '',
'unflag_denied_text' => '',
// The link type used by the flag, as defined in
// hook_flag_link_type_info().
'link_type' => 'toggle',
'weight' => 0,
);
// Merge in options from the current link type.
$link_type = $this
->get_link_type();
$options = array_merge($options, $link_type['options']);
// Allow other modules to change the flag options.
drupal_alter('flag_options', $options, $this);
return $options;
}
/**
* Provides a form for setting options.
*
* Derived classes should want to override this.
*/
function options_form(&$form) {
}
/**
* Default constructor. Loads the default options.
*/
function construct() {
$options = $this
->options();
foreach ($options as $option => $value) {
$this->{$option} = $value;
}
}
/**
* Load this flag's role data from permissions.
*
* Loads an array of roles into the flag, where each key is an action ('flag'
* and 'unflag'), and each value is a flat array of role ids which may perform
* that action.
*
* This should only be used when a complete overview of a flag's permissions
* is needed. Use $flag->access or $flag->user_access() instead.
*/
function fetch_roles() {
$actions = array(
'flag',
'unflag',
);
foreach ($actions as $action) {
// Build the permission string.
$permission = "{$action} {$this->name}";
// We want a flat array of rids rather than $rid => $role_name.
$this->roles[$action] = array_keys(user_roles(FALSE, $permission));
}
}
/**
* Update the flag with settings entered in a form.
*/
function form_input($form_values) {
// Load the form fields indiscriminately unto the flag (we don't care about
// stray FormAPI fields because we aren't touching unknown properties
// anyway).
foreach ($form_values as $field => $value) {
$this->{$field} = $value;
}
$this->types = array_values(array_filter($this->types));
// Clear internal titles cache:
$this
->get_title(NULL, TRUE);
}
/**
* Validates this flag's options.
*
* @return
* A list of errors encountered while validating this flag's options.
*/
function validate() {
// TODO: It might be nice if this used automatic method discovery rather
// than hard-coding the list of validate functions.
return array_merge_recursive($this
->validate_name(), $this
->validate_access());
}
/**
* Validates that the current flag's name is valid.
*
* @return
* A list of errors encountered while validating this flag's name.
*/
function validate_name() {
$errors = array();
// Ensure a safe machine name.
if (!preg_match('/^[a-z_][a-z0-9_]*$/', $this->name)) {
$errors['name'][] = array(
'error' => 'flag_name_characters',
'message' => t('The flag name may only contain lowercase letters, underscores, and numbers.'),
);
}
// Ensure the machine name is unique.
$flag = flag_get_flag($this->name);
if (!empty($flag) && (!isset($this->fid) || $flag->fid != $this->fid)) {
$errors['name'][] = array(
'error' => 'flag_name_unique',
'message' => t('Flag names must be unique. This flag name is already in use.'),
);
}
return $errors;
}
/**
* Validates that the current flag's access settings are valid.
*/
function validate_access() {
$errors = array();
// Require an unflag access denied message a role is not allowed to unflag.
if (empty($this->unflag_denied_text)) {
foreach ($this->roles['flag'] as $key => $rid) {
if ($rid && empty($this->roles['unflag'][$key])) {
$errors['unflag_denied_text'][] = array(
'error' => 'flag_denied_text_required',
'message' => t('The "Unflag not allowed text" is required if any user roles are not allowed to unflag.'),
);
break;
}
}
}
// Do not allow unflag access without flag access.
foreach ($this->roles['unflag'] as $key => $rid) {
if ($rid && empty($this->roles['flag'][$key])) {
$errors['roles'][] = array(
'error' => 'flag_roles_unflag',
'message' => t('Any user role that has the ability to unflag must also have the ability to flag.'),
);
break;
}
}
return $errors;
}
/**
* Fetches, possibly from some cache, an entity this flag works with.
*/
function fetch_entity($entity_id, $object_to_remember = NULL) {
static $cache = array();
if (isset($object_to_remember)) {
$cache[$entity_id] = $object_to_remember;
}
if (!array_key_exists($entity_id, $cache)) {
$entity = $this
->_load_entity($entity_id);
$cache[$entity_id] = $entity ? $entity : NULL;
}
return $cache[$entity_id];
}
/**
* Loads an entity this flag works with.
* Derived classes must implement this.
*
* @abstract
* @private
* @static
*/
function _load_entity($entity_id) {
return NULL;
}
/**
* Store an object in the flag handler's cache.
*
* This is needed because otherwise fetch_object() loads the object from the
* database (by calling _load_entity()), whereas sometimes we want to fetch
* an object that hasn't yet been saved to the database. Subsequent calls to
* fetch_entity() return the remembered object.
*
* @param int $entity_id
* The ID of the object to cache.
* @param stdClass $object
* The object to cache.
*/
function remember_entity($entity_id, $object) {
$this
->fetch_entity($entity_id, $object);
}
/**
* @defgroup access Access control
* @{
*/
/**
* Returns TRUE if the flag applies to the given entity.
*
* Derived classes must implement this.
*
* @abstract
*/
function applies_to_entity($entity) {
return FALSE;
}
/**
* Returns TRUE if the flag applies to the entity with the given ID.
*
* This is a convenience method that simply loads the object and calls
* applies_to_entity(). If you already have the object, don't call
* this function: call applies_to_entity() directly.
*/
function applies_to_entity_id($entity_id) {
return $this
->applies_to_entity($this
->fetch_entity($entity_id));
}
/**
* Provides permissions for this flag.
*
* @return
* An array of permissions for hook_permission().
*/
function get_permissions() {
return array(
"flag {$this->name}" => array(
'title' => t('Flag %flag_title', array(
'%flag_title' => $this->title,
)),
),
"unflag {$this->name}" => array(
'title' => t('Unflag %flag_title', array(
'%flag_title' => $this->title,
)),
),
);
}
/**
* Determines whether the user has the permission to use this flag.
*
* @param string $action
* (optional) The action to test, either "flag" or "unflag". If none given,
* "flag" will be tested, which is the minimum permission to use a flag.
* @param $account
* (optional) The user object. If none given, the current user will be used.
*
* @return bool
* Boolean TRUE if the user is allowed to flag/unflag. FALSE otherwise.
*
* @see flag_permission()
*/
function user_access($action = 'flag', $account = NULL) {
if (!isset($account)) {
$account = $GLOBALS['user'];
}
// Anonymous user can't use this system unless Session API is installed.
if ($account->uid == 0 && !module_exists('session_api')) {
return FALSE;
}
$permission_string = "{$action} {$this->name}";
return user_access($permission_string, $account);
}
/**
* Determines whether the user may flag, or unflag, the given entity.
*
* This method typically should not be overridden by child classes. Instead
* they should implement type_access(), which is called by this method.
*
* @param int $entity_id
* The entity ID to flag/unflag.
* @param string|NULL $action
* The action to test. Either 'flag' or 'unflag'. Leave NULL to determine
* by flag status.
* @param stdClass $account
* The user on whose behalf to test the flagging action. Leave NULL for the
* current user.
*
* @return bool
* Boolean TRUE if the user is allowed to flag/unflag the given entity.
* FALSE otherwise.
*/
function access($entity_id, $action = NULL, $account = NULL) {
if (!isset($account)) {
$account = $GLOBALS['user'];
}
if (isset($entity_id) && !$this
->applies_to_entity_id($entity_id)) {
// Flag does not apply to this entity.
return FALSE;
}
if (!isset($action)) {
$uid = $account->uid;
$sid = flag_get_sid($uid);
$action = $this
->is_flagged($entity_id, $uid, $sid) ? 'unflag' : 'flag';
}
// Base initial access on the user's basic permission to use this flag.
$access = $this
->user_access($action, $account);
// Check for additional access rules provided by sub-classes.
$child_access = $this
->type_access($entity_id, $action, $account);
if (isset($child_access)) {
$access = $child_access;
}
// Allow modules to disallow (or allow) access to flagging.
// We grant access to the flag if both of the following conditions are met:
// - No modules say to deny access.
// - At least one module says to grant access.
// If no module specified either allow or deny, we fall back to the
// default access check above.
$module_access = module_invoke_all('flag_access', $this, $entity_id, $action, $account);
if (in_array(FALSE, $module_access, TRUE)) {
$access = FALSE;
}
elseif (in_array(TRUE, $module_access, TRUE)) {
// WARNING: This allows modules to bypass the default access check!
$access = TRUE;
}
return $access;
}
/**
* Determine access to multiple objects.
*
* Similar to user_access() but works on multiple IDs at once. Called in the
* pre_render() stage of the 'Flag links' field within Views to find out where
* that link applies. The reason we do a separate DB query, and not lump this
* test in the Views query, is to make 'many to one' tests possible without
* interfering with the rows, and also to reduce the complexity of the code.
*
* This method typically should not be overridden by child classes. Instead
* they should implement type_access_multiple(), which is called by this
* method.
*
* @param array $entity_ids
* The array of entity IDs to check. The keys are the entity IDs, the
* values are the actions to test: either 'flag' or 'unflag'.
* @param stdClass $account
* (optional) The account for which the actions will be compared against.
* If left empty, the current user will be used.
*
* @return array
* An array whose keys are the object IDs and values are booleans indicating
* access.
*
* @see hook_flag_access_multiple()
*/
function access_multiple($entity_ids, $account = NULL) {
$account = isset($account) ? $account : $GLOBALS['user'];
$access = array();
// First check basic user access for this action.
foreach ($entity_ids as $entity_id => $action) {
$access[$entity_id] = $this
->user_access($entity_ids[$entity_id], $account);
}
// Check for additional access rules provided by sub-classes.
$child_access = $this
->type_access_multiple($entity_ids, $account);
if (isset($child_access)) {
foreach ($child_access as $entity_id => $entity_access) {
if (isset($entity_access)) {
$access[$entity_id] = $entity_access;
}
}
}
// Merge in module-defined access.
foreach (module_implements('flag_access_multiple') as $module) {
$module_access = module_invoke($module, 'flag_access_multiple', $this, $entity_ids, $account);
foreach ($module_access as $entity_id => $entity_access) {
if (isset($entity_access)) {
$access[$entity_id] = $entity_access;
}
}
}
return $access;
}
/**
* Implements access() implemented by each child class.
*
* @abstract
*
* @return
* FALSE if access should be denied, or NULL if there is no restriction to
* be made. This should NOT return TRUE.
*/
function type_access($entity_id, $action, $account) {
return NULL;
}
/**
* Implements access_multiple() implemented by each child class.
*
* @abstract
*
* @return
* An array keyed by entity ids, whose values represent the access to the
* corresponding entity. The access value may be FALSE if access should be
* denied, or NULL (or not set) if there is no restriction to be made. It
* should NOT be TRUE.
*/
function type_access_multiple($entity_ids, $account) {
return array();
}
/**
* @} End of "defgroup access".
*/
/**
* Given an entity, returns its ID.
* Derived classes must implement this.
*
* @abstract
*/
function get_entity_id($entity) {
return NULL;
}
/**
* Utility function: Checks whether a flag applies to a certain type, and
* possibly subtype, of entity.
*
* @param string $entity_type
* The type of entity being checked, such as "node".
* @param string|NULL $content_subtype
* The subtype being checked. For entities this will be the bundle name (the
* node type in the case of nodes).
*
* @return
* TRUE if the flag is enabled for this type and subtype.
*/
function access_entity_enabled($entity_type, $content_subtype = NULL) {
$entity_type_matches = $this->entity_type == $entity_type;
$sub_type_matches = FALSE;
if (!isset($content_subtype) || !count($this->types)) {
// Subtype automatically matches if we're not asked about it,
// or if the flag applies to all subtypes.
$sub_type_matches = TRUE;
}
else {
$sub_type_matches = in_array($content_subtype, $this->types);
}
return $entity_type_matches && $sub_type_matches;
}
/**
* Determine whether the flag should show a flag link in entity links.
*
* Derived classes are likely to implement this.
*
* @param $view_mode
* The view mode of the entity being displayed.
*
* @return
* A boolean indicating whether the flag link is to be shown in entity
* links.
*/
function shows_in_entity_links($view_mode) {
return FALSE;
}
/**
* Returns TRUE if this flag requires anonymous user cookies.
*/
function uses_anonymous_cookies() {
global $user;
return $user->uid == 0 && variable_get('cache', 0);
}
/**
* Flags, or unflags, an item.
*
* @param string $action
* Either 'flag' or 'unflag'.
* @param int $entity_id
* The ID of the item to flag or unflag.
* @param string|NULL $account
* The user on whose behalf to flag. Leave empty for the current user.
* @param bool $skip_permission_check
* Flag the item even if the $account user don't have permission to do so.
* @param $flagging
* (optional) This method works in tandem with Drupal's Field subsystem.
* Pass in a Flagging entity if you want operate on it as well. This may be
* used either of the following cases:
* - to save field data on a new Flagging entity at the same time as
* flagging an entity. In this case, using Entity API's entity_create()
* is recommended, although the Flagging entity may also be created
* directly as a new stdClass object.
* - to update field data an existing flagging. The $action parameter should
* be set to 'flag'. The Flagging entity will need to be loaded first with
* flagging_load().
* As with Drupal core API functions for saving entities, no validation of
* Field API fields is performed here. It is the responsibility of the caller
* to take care of Field API validation, using either
* field_attach_form_validate() or field_attach_validate().
*
* @return bool
* FALSE if some error occured (e.g., user has no permission, flag isn't
* applicable to the item, etc.), TRUE otherwise.
*/
function flag($action, $entity_id, $account = NULL, $skip_permission_check = FALSE, $flagging = NULL) {
// Get the user.
if (!isset($account)) {
$account = $GLOBALS['user'];
}
// Check access and applicability.
if (!$skip_permission_check) {
if (!$this
->access($entity_id, $action, $account)) {
$this->errors['access-denied'] = t('You are not allowed to flag, or unflag, this content.');
// User has no permission to flag/unflag this object.
return FALSE;
}
}
else {
// We are skipping permission checks. However, at a minimum we must make
// sure the flag applies to this entity type:
if (!$this
->applies_to_entity_id($entity_id)) {
$this->errors['entity-type'] = t('This flag does not apply to this entity type.');
return FALSE;
}
}
if ($this->errors = module_invoke_all('flag_validate', $action, $this, $entity_id, $account, $skip_permission_check, $flagging)) {
return FALSE;
}
// Find out which user id to use.
$uid = $this->global ? 0 : $account->uid;
// Find out which session id to use.
if ($this->global) {
$sid = 0;
}
else {
$sid = flag_get_sid($uid, TRUE);
// Anonymous users must always have a session id.
if ($sid == 0 && $account->uid == 0) {
$this->errors['session'] = t('Internal error: You are anonymous but you have no session ID.');
return FALSE;
}
}
// @todo: Discuss: Core wraps everything in a try { }, should we?
$existing_flagging_id = $this
->_is_flagged($entity_id, $uid, $sid);
$flagged = (bool) $existing_flagging_id;
// Ensure we have a Flagging entity and it is correctly formed.
if (isset($flagging)) {
// We were given a Flagging entity.
// Ensure that it has the uid and sid that we were also given.
$flagging->uid = $uid;
$flagging->sid = $sid;
// This is an ugly hack to preserve previous behaviour.
$flagging->given_as_parameter = TRUE;
}
else {
// We were not given a Flagging entity.
if ($flagged) {
// Load the existing Flagging entity.
$flagging = flagging_load($existing_flagging_id);
}
else {
// Construct a new Flagging entity to flag with.
$flagging = $this
->new_flagging($entity_id, $uid, $sid);
}
}
// Perform the flagging or unflagging of this flag
// along with a transaction mechanism.
$transaction = db_transaction();
try {
if ($action == 'unflag') {
if ($flagged) {
$this
->flagging_delete($flagging, $entity_id, $account);
}
// We do nothing in the case of an attempt to unflag something that isn't
// actually flagged.
}
elseif ($action == 'flag') {
if (!$flagged) {
$this
->flagging_insert($flagging, $entity_id, $account);
}
else {
$this
->flagging_update($flagging, $entity_id, $account);
}
}
} catch (Exception $e) {
$transaction
->rollback();
watchdog_exception('flag', $e);
throw $e;
}
return TRUE;
}
/**
* Create a new Flagging to flag an entity.
*
* @param $flagging
* The flagging entity that is to be saved.
* @param $entity_id
* The entity ID of entity being flagged.
* @param $account
* The account performing the flagging.
*/
private function flagging_insert($flagging, $entity_id, $account) {
if ($this
->uses_anonymous_cookies()) {
$this
->_flag_anonymous($entity_id);
}
// Invoke presave hooks.
field_attach_presave('flagging', $flagging);
// Invoke hook_entity_presave().
module_invoke_all('entity_presave', $flagging, 'flagging');
// Set the timestamp.
$flagging->timestamp = REQUEST_TIME;
// Save the flagging entity.
drupal_write_record('flagging', $flagging);
// Clear various caches; we don't want code running after us to report
// wrong counts or false flaggings.
drupal_static_reset('flag_get_user_flags');
drupal_static_reset('flag_get_entity_flags');
// Despite being named in the same pattern as the count API functions, these
// query the {flagging} table, so are reset here.
drupal_static_reset('flag_get_entity_flag_counts');
drupal_static_reset('flag_get_user_flag_counts');
$this
->_increase_count($entity_id);
// We're writing out a flagging entity even when we aren't passed one
// (e.g., when flagging via JavaScript toggle links); in this case
// Field API will assign the fields their default values.
// Invoke insert hooks.
field_attach_insert('flagging', $flagging);
// Invoke hook_entity_insert().
module_invoke_all('entity_insert', $flagging, 'flagging');
module_invoke_all('flag_flag', $this, $entity_id, $account, $flagging);
// Invoke Rules event.
if (module_exists('rules')) {
$this
->invoke_rules_event('flag', $flagging, $entity_id, $account);
}
}
/**
* Update a Flagging.
*
* @param $flagging
* The flagging entity that is being updated.
* @param $entity_id
* The entity ID of entity the flagging is on.
* @param $account
* The account performing the action.
*/
private function flagging_update($flagging, $entity_id, $account) {
// Invoke presave hooks.
// This is technically still a presave, even though the {flagging} table
// itself is not changed.
field_attach_presave('flagging', $flagging);
// Invoke hook_entity_presave().
module_invoke_all('entity_presave', $flagging, 'flagging');
// This check exists solely to preserve previous behaviour with re-flagging.
// TODO: consider removing it.
if (!empty($flagging->given_as_parameter)) {
field_attach_update('flagging', $flagging);
// Update the cache.
entity_get_controller('flagging')
->resetCache();
// Invoke hook_entity_update().
// Since there are no fields on the {flagging} table that can be
// meaningfully changed, we don't perform an update on it. However, this
// technically still counts as updating the flagging entity, since we update
// its fields.
module_invoke_all('entity_update', $flagging, 'flagging');
}
}
/**
* Unflag an entity by deleting a Flagging.
*
* @param $flagging
* The flagging entity that is to be removed.
* @param $entity_id
* The entity ID of entity being unflagged.
* @param $account
* The account performing the unflagging.
*/
private function flagging_delete($flagging, $entity_id, $account) {
if ($this
->uses_anonymous_cookies()) {
$this
->_unflag_anonymous($entity_id);
}
$transaction = db_transaction();
try {
// Note the order: We decrease the count first so hooks have accurate
// data, then invoke hooks, then delete the flagging entity.
$this
->_decrease_count($entity_id);
module_invoke_all('flag_unflag', $this, $entity_id, $account, $flagging);
// Invoke Rules event.
if (module_exists('rules')) {
$this
->invoke_rules_event('unflag', $flagging, $entity_id, $account);
}
// Invoke hook_entity_delete().
module_invoke_all('entity_delete', $flagging, 'flagging');
// Delete field data.
field_attach_delete('flagging', $flagging);
// Delete the flagging entity.
db_delete('flagging')
->condition('flagging_id', $flagging->flagging_id)
->execute();
// Remove from the cache.
entity_get_controller('flagging')
->resetCache();
// Clear various caches; we don't want code running after us to report
// wrong counts or false flaggings.
drupal_static_reset('flag_get_user_flags');
drupal_static_reset('flag_get_entity_flags');
// Despite being named in the same pattern as the count API functions, these
// query the {flagging} table, so are reset here.
drupal_static_reset('flag_get_entity_flag_counts');
drupal_static_reset('flag_get_user_flag_counts');
} catch (Exception $e) {
$transaction
->rollback();
watchdog_exception('flag', $e);
throw $e;
}
}
/**
* Invoke a Rules event in reaction to a flagging or unflagging.
*
* @param $action
* Either 'flag' or 'unflag'.
* @param $flagging
* The flagging entity that is either newly created or about to be deleted.
* @param $entity_id
* The entity ID of entity being flagged or unflagged.
* @param $account
* The account performing the action.
*/
protected function invoke_rules_event($action, $flagging, $entity_id, $account) {
// We only support flags on entities: do nothing in this class.
// See flag_entity::invoke_rules_event().
return;
}
/**
* Construct a new, empty flagging entity object.
*
* @param mixed $entity_id
* The unique identifier of the object being flagged.
* @param int $uid
* (optional) The user id of the user doing the flagging.
* @param mixed $sid
* (optional) The user SID (provided by Session API) who is doing the
* flagging. The SID is 0 for logged in users.
*
* @return stdClass
* The returned object has at least the 'flag_name' property set, which
* enables Field API to figure out the bundle, but it's your responsibility
* to eventually populate 'entity_id' and 'flagging_id'.
*/
function new_flagging($entity_id = NULL, $uid = NULL, $sid = NULL) {
return (object) array(
'flagging_id' => NULL,
'flag_name' => $this->name,
'fid' => $this->fid,
'entity_type' => $this->entity_type,
'entity_id' => $entity_id,
'uid' => $uid,
'sid' => $sid,
);
}
/**
* Determines if a certain user has flagged this object.
*
* Thanks to using a cache, inquiring several different flags about the same
* item results in only one SQL query.
*
* @param int $uid
* (optional) The user ID whose flags we're checking. If none given, the
* current user will be used.
*
* @return bool
* TRUE if the object is flagged, FALSE otherwise.
*/
function is_flagged($entity_id, $uid = NULL, $sid = NULL) {
return (bool) $this
->get_flagging_record($entity_id, $uid, $sid);
}
/**
* Returns the flagging record.
*
* This method returns the "flagging record": the {flagging} record that
* exists for each flagged item (for a certain user). If the item isn't
* flagged, returns NULL. This method could be useful, for example, when you
* want to find out the 'flagging_id' or 'timestamp' values.
*
* Thanks to using a cache, inquiring several different flags about the same
* item results in only one SQL query.
*
* Parameters are the same as is_flagged()'s.
*/
function get_flagging_record($entity_id, $uid = NULL, $sid = NULL) {
$uid = $this->global ? 0 : (!isset($uid) ? $GLOBALS['user']->uid : $uid);
$sid = $this->global ? 0 : (!isset($sid) ? flag_get_sid($uid) : $sid);
// Get all the flaggings for this user on the entity from
// flag_get_user_flags(), which will statically cache them. This means that
// when this method is called multiple times for all the flags on an entity,
// only the first call incurs a database query.
$user_flags = flag_get_user_flags($this->entity_type, $entity_id, $uid, $sid);
return isset($user_flags[$this->name]) ? $user_flags[$this->name] : NULL;
}
/**
* Similar to is_flagged() excepts it returns the flagging entity.
*/
function get_flagging($entity_id, $uid = NULL, $sid = NULL) {
if ($record = $this
->get_flagging_record($entity_id, $uid, $sid)) {
return flagging_load($record->flagging_id);
}
}
/**
* Determines if a certain user has flagged this object.
*
* You probably shouldn't call this raw private method: call the
* is_flagged() method instead.
*
* This method is similar to is_flagged() except that it does direct SQL and
* doesn't do caching. Use it when you want to not affect the cache, or to
* bypass it.
*
* @return
* If the object is flagged, returns the value of the 'flagging_id' column.
* Else, returns FALSE.
*
* @private
*/
function _is_flagged($entity_id, $uid, $sid) {
return db_select('flagging', 'fc')
->fields('fc', array(
'flagging_id',
))
->condition('fid', $this->fid)
->condition('uid', $uid)
->condition('sid', $sid)
->condition('entity_id', $entity_id)
->execute()
->fetchField();
}
/**
* Increases the flag count for an object and clears the static counts cache.
*
* @param int $entity_id
* For which item should the count be increased.
* @param int $number
* The amount of counts to increasing. Defaults to 1.
*
* @private
*/
function _increase_count($entity_id, $number = 1) {
db_merge('flag_counts')
->key(array(
'fid' => $this->fid,
'entity_id' => $entity_id,
))
->fields(array(
'entity_type' => $this->entity_type,
'count' => $number,
'last_updated' => REQUEST_TIME,
))
->updateFields(array(
'last_updated' => REQUEST_TIME,
))
->expression('count', 'count + :inc', array(
':inc' => $number,
))
->execute();
// Reset the static cache of flag counts, so code running after this gets
// correct counts.
drupal_static_reset('flag_get_counts');
drupal_static_reset('flag_get_flag_counts');
}
/**
* Decreases the flag count for an object and clears the static counts cache.
*
* @param int $entity_id
* For which item should the count be descreased.
* @param int $number
* The amount of counts to decrease. Defaults to 1.
*
* @private
*/
function _decrease_count($entity_id, $number = 1) {
// Delete rows with count 0, for data consistency and space-saving.
// Done before the db_update() to prevent out-of-bounds errors on "count".
db_delete('flag_counts')
->condition('fid', $this->fid)
->condition('entity_id', $entity_id)
->condition('count', $number, '<=')
->execute();
// Update the count with the new value otherwise.
db_update('flag_counts')
->expression('count', 'count - :inc', array(
':inc' => $number,
))
->fields(array(
'last_updated' => REQUEST_TIME,
))
->condition('fid', $this->fid)
->condition('entity_id', $entity_id)
->execute();
// Reset the static cache of flag counts, so code running after this gets
// correct counts.
drupal_static_reset('flag_get_counts');
drupal_static_reset('flag_get_flag_counts');
}
/**
* Set a cookie for anonymous users to record their flagging.
*
* @private
*/
function _flag_anonymous($entity_id) {
$storage = FlagCookieStorage::factory($this);
$storage
->flag($entity_id);
}
/**
* Remove the cookie for anonymous users to record their unflagging.
*
* @private
*/
function _unflag_anonymous($entity_id) {
$storage = FlagCookieStorage::factory($this);
$storage
->unflag($entity_id);
}
/**
* Returns the number of times an item is flagged.
*
* Thanks to using a cache, inquiring several different flags about the same
* item results in only one SQL query.
*/
function get_count($entity_id) {
$counts = flag_get_counts($this->entity_type, $entity_id);
return isset($counts[$this->name]) ? $counts[$this->name] : 0;
}
/**
* Returns the number of items a user has flagged.
*
* For global flags, pass '0' as the user ID and session ID.
*/
function get_user_count($uid, $sid = NULL) {
if (!isset($sid)) {
$sid = flag_get_sid($uid);
}
return db_select('flagging', 'fc')
->fields('fc', array(
'flagging_id',
))
->condition('fid', $this->fid)
->condition('uid', $uid)
->condition('sid', $sid)
->countQuery()
->execute()
->fetchField();
}
/**
* Processes a flag label for display. This means language translation and
* token replacements.
*
* You should always call this function and not get at the label directly.
* E.g., do `print $flag->get_label('title')` instead of `print
* $flag->title`.
*
* @param string $label
* The label to get, e.g. 'title', 'flag_short', 'unflag_short', etc.
* @param int $entity_id
* The ID in whose context to interpret tokens. If not given, only global
* tokens will be substituted.
*
* @return string
* The processed label.
*/
function get_label($label, $entity_id = NULL) {
if (!isset($this->{$label})) {
return;
}
$label = t($this->{$label});
if (strpos($label, '[') !== FALSE) {
$label = $this
->replace_tokens($label, array(), array(
'sanitize' => FALSE,
), $entity_id);
}
return filter_xss_admin($label);
}
/**
* Get the link type for this flag.
*/
function get_link_type() {
$link_types = flag_get_link_types();
return isset($this->link_type) && isset($link_types[$this->link_type]) ? $link_types[$this->link_type] : $link_types['normal'];
}
/**
* Replaces tokens in a label. Only the 'global' token context is recognized
* by default, so derived classes should override this method to add all
* token contexts they understand.
*/
function replace_tokens($label, $contexts, $options, $entity_id) {
if (strpos($label, 'flagging:') !== FALSE) {
if ($flagging = $this
->get_flagging($entity_id)) {
$contexts['flagging'] = $flagging;
}
}
return token_replace($label, $contexts, $options);
}
/**
* Returns the token types this flag understands in labels. These are used
* for narrowing down the token list shown in the help box to only the
* relevant ones.
*
* Derived classes should override this.
*/
function get_labels_token_types() {
return array(
'flagging',
);
}
/**
* A convenience method for getting the flag title.
*
* `$flag->get_title()` is shorthand for `$flag->get_label('title')`.
*/
function get_title($entity_id = NULL, $reset = FALSE) {
static $titles = array();
if ($reset) {
$titles = array();
}
$slot = intval($entity_id);
// Convert NULL to 0.
if (!isset($titles[$this->fid][$slot])) {
$titles[$this->fid][$slot] = $this
->get_label('title', $entity_id);
}
return $titles[$this->fid][$slot];
}
/**
* Returns a 'flag action' object. It exists only for the sake of its
* informative tokens. Currently, it's utilized only for the 'mail' action.
*
* Derived classes should populate the 'content_title' and 'content_url'
* slots.
*/
function get_flag_action($entity_id) {
$flag_action = new stdClass();
$flag_action->flag = $this->name;
$flag_action->entity_type = $this->entity_type;
$flag_action->entity_id = $entity_id;
return $flag_action;
}
/**
* Returns an array of errors set during validation.
*/
function get_errors() {
return $this->errors;
}
/**
* @addtogroup actions
* @{
* Methods that can be overridden to support Actions.
*/
/**
* Returns an array of all actions that are executable with this flag.
*/
function get_valid_actions() {
$actions = module_invoke_all('action_info');
foreach ($actions as $callback => $action) {
if ($action['type'] != $this->entity_type && !in_array('any', $action['triggers'])) {
unset($actions[$callback]);
}
}
return $actions;
}
/**
* Returns objects the action may possibly need. This method should return at
* least the 'primary' object the action operates on.
*
* This method is needed because get_valid_actions() returns actions that
* don't necessarily operate on an object of a type this flag manages. For
* example, flagging a comment may trigger an 'Unpublish post' action on a
* node; So the comment flag needs to tell the action about some node.
*
* Derived classes must implement this.
*
* @abstract
*/
function get_relevant_action_objects($entity_id) {
return array();
}
/**
* @} End of "addtogroup actions".
*/
/**
* @addtogroup views
* @{
* Methods that can be overridden to support the Views module.
*/
/**
* Returns information needed for Views integration. E.g., the Views table
* holding the flagged object, its primary key, and various labels. See
* derived classes for examples.
*
* @static
*/
function get_views_info() {
return array();
}
/**
* @} End of "addtogroup views".
*/
/**
* Saves a flag to the database. It is a wrapper around update() and insert().
*/
function save() {
// Allow the 'global' property to be a boolean, particularly when defined in
// hook_flag_default_flags(). Without this, a value of FALSE gets casted to
// an empty string which violates our schema. Other boolean properties are
// fine, as they are serialized.
$this->global = (int) $this->global;
if (isset($this->fid)) {
$this
->update();
$this->is_new = FALSE;
}
else {
$this
->insert();
$this->is_new = TRUE;
}
// Clear the page cache for anonymous users.
cache_clear_all('*', 'cache_page', TRUE);
}
/**
* Saves an existing flag to the database. Better use save().
*/
function update() {
db_update('flag')
->fields(array(
'name' => $this->name,
'title' => $this->title,
'global' => $this->global,
'options' => $this
->get_serialized_options(),
))
->condition('fid', $this->fid)
->execute();
db_delete('flag_types')
->condition('fid', $this->fid)
->execute();
foreach ($this->types as $type) {
db_insert('flag_types')
->fields(array(
'fid' => $this->fid,
'type' => $type,
))
->execute();
}
}
/**
* Saves a new flag to the database. Better use save().
*/
function insert() {
$this->fid = db_insert('flag')
->fields(array(
'entity_type' => $this->entity_type,
'name' => $this->name,
'title' => $this->title,
'global' => $this->global,
'options' => $this
->get_serialized_options(),
))
->execute();
foreach ($this->types as $type) {
db_insert('flag_types')
->fields(array(
'fid' => $this->fid,
'type' => $type,
))
->execute();
}
}
/**
* Options are stored serialized in the database.
*/
function get_serialized_options() {
$option_names = array_keys($this
->options());
$options = array();
foreach ($option_names as $option) {
$options[$option] = $this->{$option};
}
return serialize($options);
}
/**
* Deletes a flag from the database.
*/
function delete() {
field_attach_delete_bundle('flagging', $this->name);
db_delete('flag')
->condition('fid', $this->fid)
->execute();
db_delete('flagging')
->condition('fid', $this->fid)
->execute();
db_delete('flag_types')
->condition('fid', $this->fid)
->execute();
db_delete('flag_counts')
->condition('fid', $this->fid)
->execute();
module_invoke_all('flag_delete', $this);
}
/**
* Returns TRUE if this flag's declared API version is compatible with this
* module.
*
* An "incompatible" flag is one exported (and now being imported or exposed
* via hook_flag_default_flags()) by a different version of the Flag module.
* An incompatible flag should be treated as a "black box": it should not be
* saved or exported because our code may not know to handle its internal
* structure.
*/
function is_compatible() {
if (isset($this->fid)) {
// Database flags are always compatible.
return TRUE;
}
else {
if (!isset($this->api_version)) {
$this->api_version = 1;
}
return $this->api_version == FLAG_API_VERSION;
}
}
/**
* Finds the "default flag" corresponding to this flag.
*
* Flags defined in code ("default flags") can be overridden. This method
* returns the default flag that is being overridden by $this. Returns NULL
* if $this overrides no default flag.
*/
function find_default_flag() {
if ($this->fid) {
$default_flags = flag_get_default_flags(TRUE);
if (isset($default_flags[$this->name])) {
return $default_flags[$this->name];
}
}
}
/**
* Reverts an overriding flag to its default state.
*
* Note that $this isn't altered. To see the reverted flag you'll have to
* call flag_get_flag($this->name) again.
*
* @return
* TRUE if the flag was reverted successfully; FALSE if there was an error;
* NULL if this flag overrides no default flag.
*/
function revert() {
if ($default_flag = $this
->find_default_flag()) {
if ($default_flag
->is_compatible()) {
$default_flag = clone $default_flag;
$default_flag->fid = $this->fid;
$default_flag
->save();
drupal_static_reset('flag_get_flags');
return TRUE;
}
else {
return FALSE;
}
}
}
/**
* Disable a flag provided by a module.
*/
function disable() {
if (isset($this->module)) {
$flag_status = variable_get('flag_default_flag_status', array());
$flag_status[$this->name] = FALSE;
variable_set('flag_default_flag_status', $flag_status);
}
}
/**
* Enable a flag provided by a module.
*/
function enable() {
if (isset($this->module)) {
$flag_status = variable_get('flag_default_flag_status', array());
$flag_status[$this->name] = TRUE;
variable_set('flag_default_flag_status', $flag_status);
}
}
/**
* Returns administrative menu path for carrying out some action.
*/
function admin_path($action) {
if ($action == 'edit') {
// Since 'edit' is the default tab, we omit the action.
return FLAG_ADMIN_PATH . '/manage/' . $this->name;
}
else {
return FLAG_ADMIN_PATH . '/manage/' . $this->name . '/' . $action;
}
}
/**
* Renders a flag/unflag link.
*
* This is a wrapper around theme('flag') that channels the call to the right
* template file.
*
* @param string $action
* The action the link is about to carry out, either "flag" or "unflag".
* @param int $entity_id
* The ID of the object to flag.
* @param array $variables
* An array of further variables to pass to theme('flag'). For the full list
* of parameters, see flag.tpl.php. Of particular interest:
* - after_flagging: Set to TRUE if this flag link is being displayed as the
* result of a flagging action.
* - errors: An array of error messages.
*
* @return string
* The HTML for the flag link.
*/
function theme($action, $entity_id, $variables = array()) {
static $js_added = array();
global $user;
$after_flagging = !empty($variables['after_flagging']);
// If the flagging user is anonymous, set a boolean for the benefit of
// JavaScript code. Currently, only our "anti-crawlers" mechanism uses it.
if ($user->uid == 0 && !isset($js_added['anonymous'])) {
$js_added['anonymous'] = TRUE;
drupal_add_js(array(
'flag' => array(
'anonymous' => TRUE,
),
), 'setting');
}
// If the flagging user is anonymous and the page cache is enabled, we
// update the links through JavaScript.
if ($this
->uses_anonymous_cookies() && !$after_flagging) {
if ($this->global) {
// In case of global flags, the JavaScript template is to contain
// the opposite of the current state.
$js_action = $action == 'flag' ? 'unflag' : 'flag';
}
else {
// In case of non-global flags, we always show the "flag!" link,
// and then replace it with the "unflag!" link through JavaScript.
$js_action = 'unflag';
$action = 'flag';
}
if (!isset($js_added[$this->name . '_' . $entity_id])) {
$js_added[$this->name . '_' . $entity_id] = TRUE;
$js_template = theme($this
->theme_suggestions(), array(
'flag' => $this,
'action' => $js_action,
'entity_id' => $entity_id,
'after_flagging' => $after_flagging,
));
drupal_add_js(array(
'flag' => array(
'templates' => array(
$this->name . '_' . $entity_id => $js_template,
),
),
), 'setting');
}
}
return theme($this
->theme_suggestions(), array(
'flag' => $this,
'action' => $action,
'entity_id' => $entity_id,
) + $variables);
}
/**
* Provides an array of possible themes to try for a given flag.
*/
function theme_suggestions() {
$suggestions = array();
$suggestions[] = 'flag__' . $this->name;
$suggestions[] = 'flag__' . $this->link_type;
$suggestions[] = 'flag';
return $suggestions;
}
/**
* A shortcut function to output the link URL.
*/
function _flag_url($path, $fragment = NULL, $absolute = TRUE) {
return url($path, array(
'fragment' => $fragment,
'absolute' => $absolute,
));
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
flag_flag:: |
property | The entity type this flag works with. | ||
flag_flag:: |
public | property | An associative array containing textual errors that may be created during validation. | |
flag_flag:: |
property | The database ID. | ||
flag_flag:: |
property | Whether this flag state should act as a single toggle to all users. | ||
flag_flag:: |
property | The flag's "machine readable" name. | ||
flag_flag:: |
property | The roles array. This can be populated by fetch_roles() when needed. | ||
flag_flag:: |
property | The human-readable title for this flag. | ||
flag_flag:: |
property | The sub-types, AKA bundles, this flag applies to. | ||
flag_flag:: |
function | Determines whether the user may flag, or unflag, the given entity. | ||
flag_flag:: |
function | Utility function: Checks whether a flag applies to a certain type, and possibly subtype, of entity. | ||
flag_flag:: |
function | Determine access to multiple objects. | ||
flag_flag:: |
function | Returns administrative menu path for carrying out some action. | ||
flag_flag:: |
function | Returns TRUE if the flag applies to the given entity. | 1 | |
flag_flag:: |
function | Returns TRUE if the flag applies to the entity with the given ID. | ||
flag_flag:: |
function | Default constructor. Loads the default options. | ||
flag_flag:: |
function | Deletes a flag from the database. | ||
flag_flag:: |
function | Disable a flag provided by a module. | ||
flag_flag:: |
function | Enable a flag provided by a module. | ||
flag_flag:: |
static | function | Create a complete flag (except an FID) from an array definition. | |
flag_flag:: |
static | function | Another factory method. Returns a new, "empty" flag; e.g., one suitable for the "Add new flag" page. | |
flag_flag:: |
static | function | Creates a flag from a database row. Returns it. | |
flag_flag:: |
function | Fetches, possibly from some cache, an entity this flag works with. | ||
flag_flag:: |
function | Load this flag's role data from permissions. | ||
flag_flag:: |
function | Finds the "default flag" corresponding to this flag. | ||
flag_flag:: |
function | Flags, or unflags, an item. | 1 | |
flag_flag:: |
private | function | Unflag an entity by deleting a Flagging. | |
flag_flag:: |
private | function | Create a new Flagging to flag an entity. | |
flag_flag:: |
private | function | Update a Flagging. | |
flag_flag:: |
function | Update the flag with settings entered in a form. | 1 | |
flag_flag:: |
function | Returns the number of times an item is flagged. | ||
flag_flag:: |
function | Given an entity, returns its ID. Derived classes must implement this. | 1 | |
flag_flag:: |
function | Returns an array of errors set during validation. | ||
flag_flag:: |
function | Similar to is_flagged() excepts it returns the flagging entity. | ||
flag_flag:: |
function | Returns the flagging record. | 2 | |
flag_flag:: |
function | Returns a 'flag action' object. It exists only for the sake of its informative tokens. Currently, it's utilized only for the 'mail' action. | 1 | |
flag_flag:: |
function | Processes a flag label for display. This means language translation and token replacements. | ||
flag_flag:: |
function | Returns the token types this flag understands in labels. These are used for narrowing down the token list shown in the help box to only the relevant ones. | 1 | |
flag_flag:: |
function | Get the link type for this flag. | ||
flag_flag:: |
function | Provides permissions for this flag. | 1 | |
flag_flag:: |
function | Returns objects the action may possibly need. This method should return at least the 'primary' object the action operates on. | 1 | |
flag_flag:: |
function | Options are stored serialized in the database. | ||
flag_flag:: |
function | A convenience method for getting the flag title. | ||
flag_flag:: |
function | Returns the number of items a user has flagged. | ||
flag_flag:: |
function | Returns an array of all actions that are executable with this flag. | ||
flag_flag:: |
function | Returns information needed for Views integration. E.g., the Views table holding the flagged object, its primary key, and various labels. See derived classes for examples. | 1 | |
flag_flag:: |
function | Saves a new flag to the database. Better use save(). | ||
flag_flag:: |
protected | function | Invoke a Rules event in reaction to a flagging or unflagging. | 1 |
flag_flag:: |
function | Returns TRUE if this flag's declared API version is compatible with this module. | ||
flag_flag:: |
function | Determines if a certain user has flagged this object. | ||
flag_flag:: |
function | Construct a new, empty flagging entity object. | ||
flag_flag:: |
function | Declares the options this flag supports, and their default values. | 1 | |
flag_flag:: |
function | Provides a form for setting options. | 2 | |
flag_flag:: |
function | Store an object in the flag handler's cache. | ||
flag_flag:: |
function | Replaces tokens in a label. Only the 'global' token context is recognized by default, so derived classes should override this method to add all token contexts they understand. | 1 | |
flag_flag:: |
function | Reverts an overriding flag to its default state. | ||
flag_flag:: |
function | Saves a flag to the database. It is a wrapper around update() and insert(). | ||
flag_flag:: |
function | Determine whether the flag should show a flag link in entity links. | 1 | |
flag_flag:: |
function | Renders a flag/unflag link. | ||
flag_flag:: |
function | Provides an array of possible themes to try for a given flag. | ||
flag_flag:: |
function | Implements access() implemented by each child class. | 1 | |
flag_flag:: |
function | Implements access_multiple() implemented by each child class. | 3 | |
flag_flag:: |
function | Saves an existing flag to the database. Better use save(). | ||
flag_flag:: |
function | Determines whether the user has the permission to use this flag. | ||
flag_flag:: |
function | Returns TRUE if this flag requires anonymous user cookies. | ||
flag_flag:: |
function | Validates this flag's options. | ||
flag_flag:: |
function | Validates that the current flag's access settings are valid. | ||
flag_flag:: |
function | Validates that the current flag's name is valid. | ||
flag_flag:: |
function | Decreases the flag count for an object and clears the static counts cache. | ||
flag_flag:: |
function | Set a cookie for anonymous users to record their flagging. | ||
flag_flag:: |
function | A shortcut function to output the link URL. | ||
flag_flag:: |
function | Increases the flag count for an object and clears the static counts cache. | ||
flag_flag:: |
function | Determines if a certain user has flagged this object. | ||
flag_flag:: |
function | Loads an entity this flag works with. Derived classes must implement this. | 1 | |
flag_flag:: |
function | Remove the cookie for anonymous users to record their unflagging. |