1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* Contains the flag_flag class.
|
6
|
* Flag type classes use an object oriented style inspired by that
|
7
|
* of Views 2.
|
8
|
*/
|
9
|
|
10
|
/**
|
11
|
* This abstract class represents a flag, or, in Views 2 terminology,
|
12
|
* "a handler".
|
13
|
*
|
14
|
* This is the base class for all flag implementations. Notable derived
|
15
|
* classes are flag_node and flag_comment.
|
16
|
*
|
17
|
* There are several ways to obtain a flag handler, operating at different
|
18
|
* levels.
|
19
|
*
|
20
|
* To load an existing flag that's defined in database or code, use one of:
|
21
|
* - flag_get_flag(), the main flag API function.
|
22
|
* - flag_load(), the loader for hook_menu().
|
23
|
* - flag_get_flags(), the main API function for loading all flags. This calls
|
24
|
* flag_get_default_flags() to get flags in code.
|
25
|
*
|
26
|
* The above all use factory methods to instantiate the object for the flag and
|
27
|
* load in its settings from configuration. The factory methods are:
|
28
|
* - flag_flag::factory_by_row(), creates a flag handler from a database row.
|
29
|
* This is used by all the API functions above.
|
30
|
* - flag_flag::factory_by_array(), creates a flag handler from a configuration
|
31
|
* array. This is used by flag_get_default_flags() and the flag import form.
|
32
|
* - flag_flag::factory_by_entity_type(), creates an empty flag handler for the
|
33
|
* given entity type. This is used when a new or dummy flag handler is
|
34
|
* required and there is no configuration yet.
|
35
|
*
|
36
|
* The factory methods in turn all call the low-level function
|
37
|
* flag_create_handler(), which obtains the correct handler for the flag, or if
|
38
|
* that can't be found, the special handler flag_broken. Finally, this calls
|
39
|
* $flag->construct() on the new handler object.
|
40
|
*/
|
41
|
class flag_flag {
|
42
|
|
43
|
/**
|
44
|
* The database ID.
|
45
|
*
|
46
|
* NULL for flags that haven't been saved to the database yet.
|
47
|
*
|
48
|
* @var integer
|
49
|
*/
|
50
|
var $fid = NULL;
|
51
|
|
52
|
/**
|
53
|
* The entity type this flag works with.
|
54
|
*
|
55
|
* @var string
|
56
|
*/
|
57
|
var $entity_type = NULL;
|
58
|
|
59
|
/**
|
60
|
* The flag's "machine readable" name.
|
61
|
*
|
62
|
* @var string
|
63
|
*/
|
64
|
var $name = '';
|
65
|
|
66
|
/**
|
67
|
* The human-readable title for this flag.
|
68
|
*
|
69
|
* @var string
|
70
|
*/
|
71
|
var $title = '';
|
72
|
|
73
|
/**
|
74
|
* Whether this flag state should act as a single toggle to all users.
|
75
|
*
|
76
|
* @var bool
|
77
|
*/
|
78
|
var $global = FALSE;
|
79
|
|
80
|
/**
|
81
|
* The sub-types, AKA bundles, this flag applies to.
|
82
|
*
|
83
|
* This may be an empty array to indicate all types apply.
|
84
|
*
|
85
|
* @var array
|
86
|
*/
|
87
|
var $types = array();
|
88
|
|
89
|
/**
|
90
|
* The roles array. This can be populated by fetch_roles() when needed.
|
91
|
*/
|
92
|
var $roles = array(
|
93
|
'flag' => array(),
|
94
|
'unflag' => array(),
|
95
|
);
|
96
|
|
97
|
/**
|
98
|
* An associative array containing textual errors that may be created during
|
99
|
* validation.
|
100
|
*
|
101
|
* The array keys should reflect the type of error being set. At this time,
|
102
|
* the only "special" behavior related to the array keys is that
|
103
|
* drupal_access_denied() is called when the key is 'access-denied' and
|
104
|
* javascript is disabled.
|
105
|
*
|
106
|
* @var array
|
107
|
*/
|
108
|
public $errors = array();
|
109
|
|
110
|
/**
|
111
|
* Creates a flag from a database row. Returns it.
|
112
|
*
|
113
|
* This is static method.
|
114
|
*
|
115
|
* The reason this isn't a non-static instance method --like Views's init()--
|
116
|
* is because the class to instantiate changes according to the 'entity_type'
|
117
|
* database column. This design pattern is known as the "Single Table
|
118
|
* Inheritance".
|
119
|
*/
|
120
|
static function factory_by_row($row) {
|
121
|
$flag = flag_create_handler($row->entity_type);
|
122
|
|
123
|
// Lump all data unto the object...
|
124
|
foreach ($row as $field => $value) {
|
125
|
$flag->$field = $value;
|
126
|
}
|
127
|
// ...but skip the following two.
|
128
|
unset($flag->options, $flag->type);
|
129
|
|
130
|
// Populate the options with the defaults.
|
131
|
$options = (array) unserialize($row->options);
|
132
|
$options += $flag->options();
|
133
|
|
134
|
// Make the unserialized options accessible as normal properties.
|
135
|
foreach ($options as $option => $value) {
|
136
|
$flag->$option = $value;
|
137
|
}
|
138
|
|
139
|
if (!empty($row->type)) {
|
140
|
// The loop loading from the database should further populate this
|
141
|
// property.
|
142
|
$flag->types[] = $row->type;
|
143
|
}
|
144
|
|
145
|
return $flag;
|
146
|
}
|
147
|
|
148
|
/**
|
149
|
* Create a complete flag (except an FID) from an array definition.
|
150
|
*/
|
151
|
static function factory_by_array($config) {
|
152
|
// Allow for flags with a missing entity type.
|
153
|
$config += array(
|
154
|
'entity_type' => FALSE,
|
155
|
);
|
156
|
$flag = flag_create_handler($config['entity_type']);
|
157
|
|
158
|
foreach ($config as $option => $value) {
|
159
|
$flag->$option = $value;
|
160
|
}
|
161
|
|
162
|
if (isset($config['locked']) && is_array($config['locked'])) {
|
163
|
$flag->locked = drupal_map_assoc($config['locked']);
|
164
|
}
|
165
|
|
166
|
return $flag;
|
167
|
}
|
168
|
|
169
|
/**
|
170
|
* Another factory method. Returns a new, "empty" flag; e.g., one suitable for
|
171
|
* the "Add new flag" page.
|
172
|
*/
|
173
|
static function factory_by_entity_type($entity_type) {
|
174
|
return flag_create_handler($entity_type);
|
175
|
}
|
176
|
|
177
|
/**
|
178
|
* Declares the options this flag supports, and their default values.
|
179
|
*
|
180
|
* Derived classes should want to override this.
|
181
|
*/
|
182
|
function options() {
|
183
|
$options = array(
|
184
|
// The text for the "flag this" link for this flag.
|
185
|
'flag_short' => '',
|
186
|
// The description of the "flag this" link.
|
187
|
'flag_long' => '',
|
188
|
// Message displayed after flagging an entity.
|
189
|
'flag_message' => '',
|
190
|
// Likewise but for unflagged.
|
191
|
'unflag_short' => '',
|
192
|
'unflag_long' => '',
|
193
|
'unflag_message' => '',
|
194
|
'unflag_denied_text' => '',
|
195
|
// The link type used by the flag, as defined in
|
196
|
// hook_flag_link_type_info().
|
197
|
'link_type' => 'toggle',
|
198
|
'weight' => 0,
|
199
|
);
|
200
|
|
201
|
// Merge in options from the current link type.
|
202
|
$link_type = $this->get_link_type();
|
203
|
$options = array_merge($options, $link_type['options']);
|
204
|
|
205
|
// Allow other modules to change the flag options.
|
206
|
drupal_alter('flag_options', $options, $this);
|
207
|
return $options;
|
208
|
}
|
209
|
/**
|
210
|
* Provides a form for setting options.
|
211
|
*
|
212
|
* Derived classes should want to override this.
|
213
|
*/
|
214
|
function options_form(&$form) {
|
215
|
}
|
216
|
|
217
|
/**
|
218
|
* Default constructor. Loads the default options.
|
219
|
*/
|
220
|
function construct() {
|
221
|
$options = $this->options();
|
222
|
foreach ($options as $option => $value) {
|
223
|
$this->$option = $value;
|
224
|
}
|
225
|
}
|
226
|
|
227
|
/**
|
228
|
* Load this flag's role data from permissions.
|
229
|
*
|
230
|
* Loads an array of roles into the flag, where each key is an action ('flag'
|
231
|
* and 'unflag'), and each value is a flat array of role ids which may perform
|
232
|
* that action.
|
233
|
*
|
234
|
* This should only be used when a complete overview of a flag's permissions
|
235
|
* is needed. Use $flag->access or $flag->user_access() instead.
|
236
|
*/
|
237
|
function fetch_roles() {
|
238
|
$actions = array('flag', 'unflag');
|
239
|
foreach ($actions as $action) {
|
240
|
// Build the permission string.
|
241
|
$permission = "$action $this->name";
|
242
|
// We want a flat array of rids rather than $rid => $role_name.
|
243
|
$this->roles[$action] = array_keys(user_roles(FALSE, $permission));
|
244
|
}
|
245
|
}
|
246
|
|
247
|
/**
|
248
|
* Update the flag with settings entered in a form.
|
249
|
*/
|
250
|
function form_input($form_values) {
|
251
|
// Load the form fields indiscriminately unto the flag (we don't care about
|
252
|
// stray FormAPI fields because we aren't touching unknown properties
|
253
|
// anyway).
|
254
|
foreach ($form_values as $field => $value) {
|
255
|
$this->$field = $value;
|
256
|
}
|
257
|
$this->types = array_values(array_filter($this->types));
|
258
|
// Clear internal titles cache:
|
259
|
$this->get_title(NULL, TRUE);
|
260
|
}
|
261
|
|
262
|
/**
|
263
|
* Validates this flag's options.
|
264
|
*
|
265
|
* @return
|
266
|
* A list of errors encountered while validating this flag's options.
|
267
|
*/
|
268
|
function validate() {
|
269
|
// TODO: It might be nice if this used automatic method discovery rather
|
270
|
// than hard-coding the list of validate functions.
|
271
|
return array_merge_recursive(
|
272
|
$this->validate_name(),
|
273
|
$this->validate_access()
|
274
|
);
|
275
|
}
|
276
|
|
277
|
/**
|
278
|
* Validates that the current flag's name is valid.
|
279
|
*
|
280
|
* @return
|
281
|
* A list of errors encountered while validating this flag's name.
|
282
|
*/
|
283
|
function validate_name() {
|
284
|
$errors = array();
|
285
|
|
286
|
// Ensure a safe machine name.
|
287
|
if (!preg_match('/^[a-z_][a-z0-9_]*$/', $this->name)) {
|
288
|
$errors['name'][] = array(
|
289
|
'error' => 'flag_name_characters',
|
290
|
'message' => t('The flag name may only contain lowercase letters, underscores, and numbers.'),
|
291
|
);
|
292
|
}
|
293
|
// Ensure the machine name is unique.
|
294
|
$flag = flag_get_flag($this->name);
|
295
|
if (!empty($flag) && (!isset($this->fid) || $flag->fid != $this->fid)) {
|
296
|
$errors['name'][] = array(
|
297
|
'error' => 'flag_name_unique',
|
298
|
'message' => t('Flag names must be unique. This flag name is already in use.'),
|
299
|
);
|
300
|
}
|
301
|
|
302
|
return $errors;
|
303
|
}
|
304
|
|
305
|
/**
|
306
|
* Validates that the current flag's access settings are valid.
|
307
|
*/
|
308
|
function validate_access() {
|
309
|
$errors = array();
|
310
|
|
311
|
// Require an unflag access denied message a role is not allowed to unflag.
|
312
|
if (empty($this->unflag_denied_text)) {
|
313
|
foreach ($this->roles['flag'] as $key => $rid) {
|
314
|
if ($rid && empty($this->roles['unflag'][$key])) {
|
315
|
$errors['unflag_denied_text'][] = array(
|
316
|
'error' => 'flag_denied_text_required',
|
317
|
'message' => t('The "Unflag not allowed text" is required if any user roles are not allowed to unflag.'),
|
318
|
);
|
319
|
break;
|
320
|
}
|
321
|
}
|
322
|
}
|
323
|
|
324
|
// Do not allow unflag access without flag access.
|
325
|
foreach ($this->roles['unflag'] as $key => $rid) {
|
326
|
if ($rid && empty($this->roles['flag'][$key])) {
|
327
|
$errors['roles'][] = array(
|
328
|
'error' => 'flag_roles_unflag',
|
329
|
'message' => t('Any user role that has the ability to unflag must also have the ability to flag.'),
|
330
|
);
|
331
|
break;
|
332
|
}
|
333
|
}
|
334
|
|
335
|
return $errors;
|
336
|
}
|
337
|
|
338
|
/**
|
339
|
* Fetches, possibly from some cache, an entity this flag works with.
|
340
|
*/
|
341
|
function fetch_entity($entity_id, $object_to_remember = NULL) {
|
342
|
static $cache = array();
|
343
|
if (isset($object_to_remember)) {
|
344
|
$cache[$entity_id] = $object_to_remember;
|
345
|
}
|
346
|
if (!array_key_exists($entity_id, $cache)) {
|
347
|
$entity = $this->_load_entity($entity_id);
|
348
|
$cache[$entity_id] = $entity ? $entity : NULL;
|
349
|
}
|
350
|
return $cache[$entity_id];
|
351
|
}
|
352
|
|
353
|
/**
|
354
|
* Loads an entity this flag works with.
|
355
|
* Derived classes must implement this.
|
356
|
*
|
357
|
* @abstract
|
358
|
* @private
|
359
|
* @static
|
360
|
*/
|
361
|
function _load_entity($entity_id) {
|
362
|
return NULL;
|
363
|
}
|
364
|
|
365
|
/**
|
366
|
* Store an object in the flag handler's cache.
|
367
|
*
|
368
|
* This is needed because otherwise fetch_object() loads the object from the
|
369
|
* database (by calling _load_entity()), whereas sometimes we want to fetch
|
370
|
* an object that hasn't yet been saved to the database. Subsequent calls to
|
371
|
* fetch_entity() return the remembered object.
|
372
|
*
|
373
|
* @param $entity_id
|
374
|
* The ID of the object to cache.
|
375
|
* @param $object
|
376
|
* The object to cache.
|
377
|
*/
|
378
|
function remember_entity($entity_id, $object) {
|
379
|
$this->fetch_entity($entity_id, $object);
|
380
|
}
|
381
|
|
382
|
/**
|
383
|
* @defgroup access Access control
|
384
|
* @{
|
385
|
*/
|
386
|
|
387
|
/**
|
388
|
* Returns TRUE if the flag applies to the given entity.
|
389
|
*
|
390
|
* Derived classes must implement this.
|
391
|
*
|
392
|
* @abstract
|
393
|
*/
|
394
|
function applies_to_entity($entity) {
|
395
|
return FALSE;
|
396
|
}
|
397
|
|
398
|
/**
|
399
|
* Returns TRUE if the flag applies to the entity with the given ID.
|
400
|
*
|
401
|
* This is a convenience method that simply loads the object and calls
|
402
|
* applies_to_entity(). If you already have the object, don't call
|
403
|
* this function: call applies_to_entity() directly.
|
404
|
*/
|
405
|
function applies_to_entity_id($entity_id) {
|
406
|
return $this->applies_to_entity($this->fetch_entity($entity_id));
|
407
|
}
|
408
|
|
409
|
/**
|
410
|
* Provides permissions for this flag.
|
411
|
*
|
412
|
* @return
|
413
|
* An array of permissions for hook_permission().
|
414
|
*/
|
415
|
function get_permissions() {
|
416
|
return array(
|
417
|
"flag $this->name" => array(
|
418
|
'title' => t('Flag %flag_title', array(
|
419
|
'%flag_title' => $this->title,
|
420
|
)),
|
421
|
),
|
422
|
"unflag $this->name" => array(
|
423
|
'title' => t('Unflag %flag_title', array(
|
424
|
'%flag_title' => $this->title,
|
425
|
)),
|
426
|
),
|
427
|
);
|
428
|
}
|
429
|
|
430
|
/**
|
431
|
* Determines whether the user has the permission to use this flag.
|
432
|
*
|
433
|
* @param $action
|
434
|
* (optional) The action to test, either "flag" or "unflag". If none given,
|
435
|
* "flag" will be tested, which is the minimum permission to use a flag.
|
436
|
* @param $account
|
437
|
* (optional) The user object. If none given, the current user will be used.
|
438
|
*
|
439
|
* @return
|
440
|
* Boolean TRUE if the user is allowed to flag/unflag. FALSE otherwise.
|
441
|
*
|
442
|
* @see flag_permission()
|
443
|
*/
|
444
|
function user_access($action = 'flag', $account = NULL) {
|
445
|
if (!isset($account)) {
|
446
|
$account = $GLOBALS['user'];
|
447
|
}
|
448
|
|
449
|
// Anonymous user can't use this system unless Session API is installed.
|
450
|
if ($account->uid == 0 && !module_exists('session_api')) {
|
451
|
return FALSE;
|
452
|
}
|
453
|
|
454
|
$permission_string = "$action $this->name";
|
455
|
return user_access($permission_string, $account);
|
456
|
}
|
457
|
|
458
|
/**
|
459
|
* Determines whether the user may flag, or unflag, the given entity.
|
460
|
*
|
461
|
* This method typically should not be overridden by child classes. Instead
|
462
|
* they should implement type_access(), which is called by this method.
|
463
|
*
|
464
|
* @param $entity_id
|
465
|
* The entity ID to flag/unflag.
|
466
|
* @param $action
|
467
|
* The action to test. Either 'flag' or 'unflag'. Leave NULL to determine
|
468
|
* by flag status.
|
469
|
* @param $account
|
470
|
* The user on whose behalf to test the flagging action. Leave NULL for the
|
471
|
* current user.
|
472
|
*
|
473
|
* @return
|
474
|
* Boolean TRUE if the user is allowed to flag/unflag the given entity.
|
475
|
* FALSE otherwise.
|
476
|
*/
|
477
|
function access($entity_id, $action = NULL, $account = NULL) {
|
478
|
if (!isset($account)) {
|
479
|
$account = $GLOBALS['user'];
|
480
|
}
|
481
|
|
482
|
if (isset($entity_id) && !$this->applies_to_entity_id($entity_id)) {
|
483
|
// Flag does not apply to this entity.
|
484
|
return FALSE;
|
485
|
}
|
486
|
|
487
|
if (!isset($action)) {
|
488
|
$uid = $account->uid;
|
489
|
$sid = flag_get_sid($uid);
|
490
|
$action = $this->is_flagged($entity_id, $uid, $sid) ? 'unflag' : 'flag';
|
491
|
}
|
492
|
|
493
|
// Base initial access on the user's basic permission to use this flag.
|
494
|
$access = $this->user_access($action, $account);
|
495
|
|
496
|
// Check for additional access rules provided by sub-classes.
|
497
|
$child_access = $this->type_access($entity_id, $action, $account);
|
498
|
if (isset($child_access)) {
|
499
|
$access = $child_access;
|
500
|
}
|
501
|
|
502
|
// Allow modules to disallow (or allow) access to flagging.
|
503
|
// We grant access to the flag if both of the following conditions are met:
|
504
|
// - No modules say to deny access.
|
505
|
// - At least one module says to grant access.
|
506
|
// If no module specified either allow or deny, we fall back to the
|
507
|
// default access check above.
|
508
|
$module_access = module_invoke_all('flag_access', $this, $entity_id, $action, $account);
|
509
|
if (in_array(FALSE, $module_access, TRUE)) {
|
510
|
$access = FALSE;
|
511
|
}
|
512
|
elseif (in_array(TRUE, $module_access, TRUE)) {
|
513
|
// WARNING: This allows modules to bypass the default access check!
|
514
|
$access = TRUE;
|
515
|
}
|
516
|
|
517
|
return $access;
|
518
|
}
|
519
|
|
520
|
/**
|
521
|
* Determine access to multiple objects.
|
522
|
*
|
523
|
* Similar to user_access() but works on multiple IDs at once. Called in the
|
524
|
* pre_render() stage of the 'Flag links' field within Views to find out where
|
525
|
* that link applies. The reason we do a separate DB query, and not lump this
|
526
|
* test in the Views query, is to make 'many to one' tests possible without
|
527
|
* interfering with the rows, and also to reduce the complexity of the code.
|
528
|
*
|
529
|
* This method typically should not be overridden by child classes. Instead
|
530
|
* they should implement type_access_multiple(), which is called by this
|
531
|
* method.
|
532
|
*
|
533
|
* @param $entity_ids
|
534
|
* The array of entity IDs to check. The keys are the entity IDs, the
|
535
|
* values are the actions to test: either 'flag' or 'unflag'.
|
536
|
* @param $account
|
537
|
* (optional) The account for which the actions will be compared against.
|
538
|
* If left empty, the current user will be used.
|
539
|
*
|
540
|
* @return
|
541
|
* An array whose keys are the object IDs and values are booleans indicating
|
542
|
* access.
|
543
|
*
|
544
|
* @see hook_flag_access_multiple()
|
545
|
*/
|
546
|
function access_multiple($entity_ids, $account = NULL) {
|
547
|
$account = isset($account) ? $account : $GLOBALS['user'];
|
548
|
$access = array();
|
549
|
|
550
|
// First check basic user access for this action.
|
551
|
foreach ($entity_ids as $entity_id => $action) {
|
552
|
$access[$entity_id] = $this->user_access($entity_ids[$entity_id], $account);
|
553
|
}
|
554
|
|
555
|
// Check for additional access rules provided by sub-classes.
|
556
|
$child_access = $this->type_access_multiple($entity_ids, $account);
|
557
|
if (isset($child_access)) {
|
558
|
foreach ($child_access as $entity_id => $entity_access) {
|
559
|
if (isset($entity_access)) {
|
560
|
$access[$entity_id] = $entity_access;
|
561
|
}
|
562
|
}
|
563
|
}
|
564
|
|
565
|
// Merge in module-defined access.
|
566
|
foreach (module_implements('flag_access_multiple') as $module) {
|
567
|
$module_access = module_invoke($module, 'flag_access_multiple', $this, $entity_ids, $account);
|
568
|
foreach ($module_access as $entity_id => $entity_access) {
|
569
|
if (isset($entity_access)) {
|
570
|
$access[$entity_id] = $entity_access;
|
571
|
}
|
572
|
}
|
573
|
}
|
574
|
|
575
|
return $access;
|
576
|
}
|
577
|
|
578
|
/**
|
579
|
* Implements access() implemented by each child class.
|
580
|
*
|
581
|
* @abstract
|
582
|
*
|
583
|
* @return
|
584
|
* FALSE if access should be denied, or NULL if there is no restriction to
|
585
|
* be made. This should NOT return TRUE.
|
586
|
*/
|
587
|
function type_access($entity_id, $action, $account) {
|
588
|
return NULL;
|
589
|
}
|
590
|
|
591
|
/**
|
592
|
* Implements access_multiple() implemented by each child class.
|
593
|
*
|
594
|
* @abstract
|
595
|
*
|
596
|
* @return
|
597
|
* An array keyed by entity ids, whose values represent the access to the
|
598
|
* corresponding entity. The access value may be FALSE if access should be
|
599
|
* denied, or NULL (or not set) if there is no restriction to be made. It
|
600
|
* should NOT be TRUE.
|
601
|
*/
|
602
|
function type_access_multiple($entity_ids, $account) {
|
603
|
return array();
|
604
|
}
|
605
|
|
606
|
/**
|
607
|
* @} End of "defgroup access".
|
608
|
*/
|
609
|
|
610
|
/**
|
611
|
* Given an entity, returns its ID.
|
612
|
* Derived classes must implement this.
|
613
|
*
|
614
|
* @abstract
|
615
|
*/
|
616
|
function get_entity_id($entity) {
|
617
|
return NULL;
|
618
|
}
|
619
|
|
620
|
/**
|
621
|
* Utility function: Checks whether a flag applies to a certain type, and
|
622
|
* possibly subtype, of entity.
|
623
|
*
|
624
|
* @param $entity_type
|
625
|
* The type of entity being checked, such as "node".
|
626
|
* @param $content_subtype
|
627
|
* The subtype being checked. For entities this will be the bundle name (the
|
628
|
* node type in the case of nodes).
|
629
|
*
|
630
|
* @return
|
631
|
* TRUE if the flag is enabled for this type and subtype.
|
632
|
*/
|
633
|
function access_entity_enabled($entity_type, $content_subtype = NULL) {
|
634
|
$entity_type_matches = ($this->entity_type == $entity_type);
|
635
|
$sub_type_matches = FALSE;
|
636
|
if (!isset($content_subtype) || !count($this->types)) {
|
637
|
// Subtype automatically matches if we're not asked about it,
|
638
|
// or if the flag applies to all subtypes.
|
639
|
$sub_type_matches = TRUE;
|
640
|
}
|
641
|
else {
|
642
|
$sub_type_matches = in_array($content_subtype, $this->types);
|
643
|
}
|
644
|
return $entity_type_matches && $sub_type_matches;
|
645
|
}
|
646
|
|
647
|
/**
|
648
|
* Determine whether the flag should show a flag link in entity links.
|
649
|
*
|
650
|
* Derived classes are likely to implement this.
|
651
|
*
|
652
|
* @param $view_mode
|
653
|
* The view mode of the entity being displayed.
|
654
|
*
|
655
|
* @return
|
656
|
* A boolean indicating whether the flag link is to be shown in entity
|
657
|
* links.
|
658
|
*/
|
659
|
function shows_in_entity_links($view_mode) {
|
660
|
return FALSE;
|
661
|
}
|
662
|
|
663
|
/**
|
664
|
* Returns TRUE if this flag requires anonymous user cookies.
|
665
|
*/
|
666
|
function uses_anonymous_cookies() {
|
667
|
global $user;
|
668
|
return $user->uid == 0 && variable_get('cache', 0);
|
669
|
}
|
670
|
|
671
|
/**
|
672
|
* Flags, or unflags, an item.
|
673
|
*
|
674
|
* @param $action
|
675
|
* Either 'flag' or 'unflag'.
|
676
|
* @param $entity_id
|
677
|
* The ID of the item to flag or unflag.
|
678
|
* @param $account
|
679
|
* The user on whose behalf to flag. Leave empty for the current user.
|
680
|
* @param $skip_permission_check
|
681
|
* Flag the item even if the $account user don't have permission to do so.
|
682
|
* @param $flagging
|
683
|
* (optional) This method works in tandem with Drupal's Field subsystem.
|
684
|
* Pass in a Flagging entity if you want operate on it as well. This may be
|
685
|
* used either of the following cases:
|
686
|
* - to save field data on a new Flagging entity at the same time as
|
687
|
* flagging an entity. In this case, using Entity API's entity_create()
|
688
|
* is recommended, although the Flagging entity may also be created
|
689
|
* directly as a new stdClass object.
|
690
|
* - to update field data an existing flagging. The $action parameter should
|
691
|
* be set to 'flag'. The Flagging entity will need to be loaded first with
|
692
|
* flagging_load().
|
693
|
* As with Drupal core API functions for saving entities, no validation of
|
694
|
* Field API fields is performed here. It is the responsibility of the caller
|
695
|
* to take care of Field API validation, using either
|
696
|
* field_attach_form_validate() or field_attach_validate().
|
697
|
*
|
698
|
* @return
|
699
|
* FALSE if some error occured (e.g., user has no permission, flag isn't
|
700
|
* applicable to the item, etc.), TRUE otherwise.
|
701
|
*/
|
702
|
function flag($action, $entity_id, $account = NULL, $skip_permission_check = FALSE, $flagging = NULL) {
|
703
|
// Get the user.
|
704
|
if (!isset($account)) {
|
705
|
$account = $GLOBALS['user'];
|
706
|
}
|
707
|
|
708
|
// Check access and applicability.
|
709
|
if (!$skip_permission_check) {
|
710
|
if (!$this->access($entity_id, $action, $account)) {
|
711
|
$this->errors['access-denied'] = t('You are not allowed to flag, or unflag, this content.');
|
712
|
// User has no permission to flag/unflag this object.
|
713
|
return FALSE;
|
714
|
}
|
715
|
}
|
716
|
else {
|
717
|
// We are skipping permission checks. However, at a minimum we must make
|
718
|
// sure the flag applies to this entity type:
|
719
|
if (!$this->applies_to_entity_id($entity_id)) {
|
720
|
$this->errors['entity-type'] = t('This flag does not apply to this entity type.');
|
721
|
return FALSE;
|
722
|
}
|
723
|
}
|
724
|
|
725
|
if (($this->errors = module_invoke_all('flag_validate', $action, $this, $entity_id, $account, $skip_permission_check, $flagging))) {
|
726
|
return FALSE;
|
727
|
}
|
728
|
|
729
|
// Find out which user id to use.
|
730
|
$uid = $this->global ? 0 : $account->uid;
|
731
|
|
732
|
// Find out which session id to use.
|
733
|
if ($this->global) {
|
734
|
$sid = 0;
|
735
|
}
|
736
|
else {
|
737
|
$sid = flag_get_sid($uid, TRUE);
|
738
|
// Anonymous users must always have a session id.
|
739
|
if ($sid == 0 && $account->uid == 0) {
|
740
|
$this->errors['session'] = t('Internal error: You are anonymous but you have no session ID.');
|
741
|
return FALSE;
|
742
|
}
|
743
|
}
|
744
|
|
745
|
// @todo: Discuss: Core wraps everything in a try { }, should we?
|
746
|
|
747
|
$existing_flagging_id = $this->_is_flagged($entity_id, $uid, $sid);
|
748
|
$flagged = (bool) $existing_flagging_id;
|
749
|
|
750
|
// Ensure we have a Flagging entity and it is correctly formed.
|
751
|
if (isset($flagging)) {
|
752
|
// We were given a Flagging entity.
|
753
|
// Ensure that it has the uid and sid that we were also given.
|
754
|
$flagging->uid = $uid;
|
755
|
$flagging->sid = $sid;
|
756
|
|
757
|
// This is an ugly hack to preserve previous behaviour.
|
758
|
$flagging->given_as_parameter = TRUE;
|
759
|
}
|
760
|
else {
|
761
|
// We were not given a Flagging entity.
|
762
|
if ($flagged) {
|
763
|
// Load the existing Flagging entity.
|
764
|
$flagging = flagging_load($existing_flagging_id);
|
765
|
}
|
766
|
else {
|
767
|
// Construct a new Flagging entity to flag with.
|
768
|
$flagging = $this->new_flagging($entity_id, $uid, $sid);
|
769
|
}
|
770
|
}
|
771
|
|
772
|
// Perform the flagging or unflagging of this flag.
|
773
|
if ($action == 'unflag') {
|
774
|
if ($flagged) {
|
775
|
$this->flagging_delete($flagging, $entity_id, $account);
|
776
|
}
|
777
|
// We do nothing in the case of an attempt to unflag something that isn't
|
778
|
// actually flagged.
|
779
|
}
|
780
|
elseif ($action == 'flag') {
|
781
|
if (!$flagged) {
|
782
|
$this->flagging_insert($flagging, $entity_id, $account);
|
783
|
}
|
784
|
else {
|
785
|
$this->flagging_update($flagging, $entity_id, $account);
|
786
|
}
|
787
|
}
|
788
|
|
789
|
return TRUE;
|
790
|
}
|
791
|
|
792
|
/**
|
793
|
* Create a new Flagging to flag an entity.
|
794
|
*
|
795
|
* @param $flagging
|
796
|
* The flagging entity that is to be saved.
|
797
|
* @param $entity_id
|
798
|
* The entity ID of entity being flagged.
|
799
|
* @param $account
|
800
|
* The account performing the flagging.
|
801
|
*/
|
802
|
private function flagging_insert($flagging, $entity_id, $account) {
|
803
|
if ($this->uses_anonymous_cookies()) {
|
804
|
$this->_flag_anonymous($entity_id);
|
805
|
}
|
806
|
|
807
|
// Invoke presave hooks.
|
808
|
field_attach_presave('flagging', $flagging);
|
809
|
// Invoke hook_entity_presave().
|
810
|
module_invoke_all('entity_presave', $flagging, 'flagging');
|
811
|
|
812
|
// Set the timestamp.
|
813
|
$flagging->timestamp = REQUEST_TIME;
|
814
|
|
815
|
// Save the flagging entity.
|
816
|
drupal_write_record('flagging', $flagging);
|
817
|
|
818
|
// Clear various caches; we don't want code running after us to report
|
819
|
// wrong counts or false flaggings.
|
820
|
drupal_static_reset('flag_get_user_flags');
|
821
|
drupal_static_reset('flag_get_entity_flags');
|
822
|
// Despite being named in the same pattern as the count API functions, these
|
823
|
// query the {flagging} table, so are reset here.
|
824
|
drupal_static_reset('flag_get_entity_flag_counts');
|
825
|
drupal_static_reset('flag_get_user_flag_counts');
|
826
|
|
827
|
$this->_increase_count($entity_id);
|
828
|
// We're writing out a flagging entity even when we aren't passed one
|
829
|
// (e.g., when flagging via JavaScript toggle links); in this case
|
830
|
// Field API will assign the fields their default values.
|
831
|
|
832
|
// Invoke insert hooks.
|
833
|
field_attach_insert('flagging', $flagging);
|
834
|
// Invoke hook_entity_insert().
|
835
|
module_invoke_all('entity_insert', $flagging, 'flagging');
|
836
|
|
837
|
module_invoke_all('flag_flag', $this, $entity_id, $account, $flagging);
|
838
|
// Invoke Rules event.
|
839
|
if (module_exists('rules')) {
|
840
|
$this->invoke_rules_event('flag', $flagging, $entity_id, $account);
|
841
|
}
|
842
|
}
|
843
|
|
844
|
/**
|
845
|
* Update a Flagging.
|
846
|
*
|
847
|
* @param $flagging
|
848
|
* The flagging entity that is being updated.
|
849
|
* @param $entity_id
|
850
|
* The entity ID of entity the flagging is on.
|
851
|
* @param $account
|
852
|
* The account performing the action.
|
853
|
*/
|
854
|
private function flagging_update($flagging, $entity_id, $account) {
|
855
|
// Invoke presave hooks.
|
856
|
// This is technically still a presave, even though the {flagging} table
|
857
|
// itself is not changed.
|
858
|
field_attach_presave('flagging', $flagging);
|
859
|
// Invoke hook_entity_presave().
|
860
|
module_invoke_all('entity_presave', $flagging, 'flagging');
|
861
|
|
862
|
// This check exists solely to preserve previous behaviour with re-flagging.
|
863
|
// TODO: consider removing it.
|
864
|
if (!empty($flagging->given_as_parameter)) {
|
865
|
field_attach_update('flagging', $flagging);
|
866
|
// Update the cache.
|
867
|
entity_get_controller('flagging')->resetCache();
|
868
|
// Invoke hook_entity_update().
|
869
|
// Since there are no fields on the {flagging} table that can be
|
870
|
// meaningfully changed, we don't perform an update on it. However, this
|
871
|
// technically still counts as updating the flagging entity, since we update
|
872
|
// its fields.
|
873
|
module_invoke_all('entity_update', $flagging, 'flagging');
|
874
|
}
|
875
|
}
|
876
|
|
877
|
/**
|
878
|
* Unflag an entity by deleting a Flagging.
|
879
|
*
|
880
|
* @param $flagging
|
881
|
* The flagging entity that is to be removed.
|
882
|
* @param $entity_id
|
883
|
* The entity ID of entity being unflagged.
|
884
|
* @param $account
|
885
|
* The account performing the unflagging.
|
886
|
*/
|
887
|
private function flagging_delete($flagging, $entity_id, $account) {
|
888
|
if ($this->uses_anonymous_cookies()) {
|
889
|
$this->_unflag_anonymous($entity_id);
|
890
|
}
|
891
|
|
892
|
$transaction = db_transaction();
|
893
|
try {
|
894
|
// Note the order: We decrease the count first so hooks have accurate
|
895
|
// data, then invoke hooks, then delete the flagging entity.
|
896
|
$this->_decrease_count($entity_id);
|
897
|
module_invoke_all('flag_unflag', $this, $entity_id, $account, $flagging);
|
898
|
// Invoke Rules event.
|
899
|
if (module_exists('rules')) {
|
900
|
$this->invoke_rules_event('unflag', $flagging, $entity_id, $account);
|
901
|
}
|
902
|
|
903
|
// Invoke hook_entity_delete().
|
904
|
module_invoke_all('entity_delete', $flagging, 'flagging');
|
905
|
// Delete field data.
|
906
|
field_attach_delete('flagging', $flagging);
|
907
|
|
908
|
// Delete the flagging entity.
|
909
|
db_delete('flagging')->condition('flagging_id', $flagging->flagging_id)->execute();
|
910
|
|
911
|
// Remove from the cache.
|
912
|
entity_get_controller('flagging')->resetCache();
|
913
|
|
914
|
// Clear various caches; we don't want code running after us to report
|
915
|
// wrong counts or false flaggings.
|
916
|
drupal_static_reset('flag_get_user_flags');
|
917
|
drupal_static_reset('flag_get_entity_flags');
|
918
|
// Despite being named in the same pattern as the count API functions, these
|
919
|
// query the {flagging} table, so are reset here.
|
920
|
drupal_static_reset('flag_get_entity_flag_counts');
|
921
|
drupal_static_reset('flag_get_user_flag_counts');
|
922
|
}
|
923
|
catch (Exception $e) {
|
924
|
$transaction->rollback();
|
925
|
watchdog_exception('flag', $e);
|
926
|
throw $e;
|
927
|
}
|
928
|
}
|
929
|
|
930
|
/**
|
931
|
* Invoke a Rules event in reaction to a flagging or unflagging.
|
932
|
*
|
933
|
* @param $action
|
934
|
* Either 'flag' or 'unflag'.
|
935
|
* @param $flagging
|
936
|
* The flagging entity that is either newly created or about to be deleted.
|
937
|
* @param $entity_id
|
938
|
* The entity ID of entity being flagged or unflagged.
|
939
|
* @param $account
|
940
|
* The account performing the action.
|
941
|
*/
|
942
|
protected function invoke_rules_event($action, $flagging, $entity_id, $account) {
|
943
|
// We only support flags on entities: do nothing in this class.
|
944
|
// See flag_entity::invoke_rules_event().
|
945
|
return;
|
946
|
}
|
947
|
|
948
|
/**
|
949
|
* Construct a new, empty flagging entity object.
|
950
|
*
|
951
|
* @param mixed $entity_id
|
952
|
* The unique identifier of the object being flagged.
|
953
|
* @param int $uid
|
954
|
* (optional) The user id of the user doing the flagging.
|
955
|
* @param mixed $sid
|
956
|
* (optional) The user SID (provided by Session API) who is doing the
|
957
|
* flagging. The SID is 0 for logged in users.
|
958
|
*
|
959
|
* @return stdClass
|
960
|
* The returned object has at least the 'flag_name' property set, which
|
961
|
* enables Field API to figure out the bundle, but it's your responsibility
|
962
|
* to eventually populate 'entity_id' and 'flagging_id'.
|
963
|
*/
|
964
|
function new_flagging($entity_id = NULL, $uid = NULL, $sid = NULL) {
|
965
|
return (object) array(
|
966
|
'flagging_id' => NULL,
|
967
|
'flag_name' => $this->name,
|
968
|
'fid' => $this->fid,
|
969
|
'entity_type' => $this->entity_type,
|
970
|
'entity_id' => $entity_id,
|
971
|
'uid' => $uid,
|
972
|
'sid' => $sid,
|
973
|
// The timestamp is not set until this is saved.
|
974
|
);
|
975
|
}
|
976
|
|
977
|
/**
|
978
|
* Determines if a certain user has flagged this object.
|
979
|
*
|
980
|
* Thanks to using a cache, inquiring several different flags about the same
|
981
|
* item results in only one SQL query.
|
982
|
*
|
983
|
* @param $uid
|
984
|
* (optional) The user ID whose flags we're checking. If none given, the
|
985
|
* current user will be used.
|
986
|
*
|
987
|
* @return
|
988
|
* TRUE if the object is flagged, FALSE otherwise.
|
989
|
*/
|
990
|
function is_flagged($entity_id, $uid = NULL, $sid = NULL) {
|
991
|
return (bool) $this->get_flagging_record($entity_id, $uid, $sid);
|
992
|
}
|
993
|
|
994
|
/**
|
995
|
* Returns the flagging record.
|
996
|
*
|
997
|
* This method returns the "flagging record": the {flagging} record that
|
998
|
* exists for each flagged item (for a certain user). If the item isn't
|
999
|
* flagged, returns NULL. This method could be useful, for example, when you
|
1000
|
* want to find out the 'flagging_id' or 'timestamp' values.
|
1001
|
*
|
1002
|
* Thanks to using a cache, inquiring several different flags about the same
|
1003
|
* item results in only one SQL query.
|
1004
|
*
|
1005
|
* Parameters are the same as is_flagged()'s.
|
1006
|
*/
|
1007
|
function get_flagging_record($entity_id, $uid = NULL, $sid = NULL) {
|
1008
|
$uid = $this->global ? 0 : (!isset($uid) ? $GLOBALS['user']->uid : $uid);
|
1009
|
$sid = $this->global ? 0 : (!isset($sid) ? flag_get_sid($uid) : $sid);
|
1010
|
|
1011
|
// flag_get_user_flags() does caching.
|
1012
|
$user_flags = flag_get_user_flags($this->entity_type, $entity_id, $uid, $sid);
|
1013
|
return isset($user_flags[$this->name]) ? $user_flags[$this->name] : NULL;
|
1014
|
}
|
1015
|
|
1016
|
/**
|
1017
|
* Similar to is_flagged() excepts it returns the flagging entity.
|
1018
|
*/
|
1019
|
function get_flagging($entity_id, $uid = NULL, $sid = NULL) {
|
1020
|
if (($record = $this->get_flagging_record($entity_id, $uid, $sid))) {
|
1021
|
return flagging_load($record->flagging_id);
|
1022
|
}
|
1023
|
}
|
1024
|
|
1025
|
/**
|
1026
|
* Determines if a certain user has flagged this object.
|
1027
|
*
|
1028
|
* You probably shouldn't call this raw private method: call the
|
1029
|
* is_flagged() method instead.
|
1030
|
*
|
1031
|
* This method is similar to is_flagged() except that it does direct SQL and
|
1032
|
* doesn't do caching. Use it when you want to not affect the cache, or to
|
1033
|
* bypass it.
|
1034
|
*
|
1035
|
* @return
|
1036
|
* If the object is flagged, returns the value of the 'flagging_id' column.
|
1037
|
* Else, returns FALSE.
|
1038
|
*
|
1039
|
* @private
|
1040
|
*/
|
1041
|
function _is_flagged($entity_id, $uid, $sid) {
|
1042
|
return db_select('flagging', 'fc')
|
1043
|
->fields('fc', array('flagging_id'))
|
1044
|
->condition('fid', $this->fid)
|
1045
|
->condition('uid', $uid)
|
1046
|
->condition('sid', $sid)
|
1047
|
->condition('entity_id', $entity_id)
|
1048
|
->execute()
|
1049
|
->fetchField();
|
1050
|
}
|
1051
|
|
1052
|
/**
|
1053
|
* Increases the flag count for an object and clears the static counts cache.
|
1054
|
*
|
1055
|
* @param $entity_id
|
1056
|
* For which item should the count be increased.
|
1057
|
* @param $number
|
1058
|
* The amount of counts to increasing. Defaults to 1.
|
1059
|
*
|
1060
|
* @private
|
1061
|
*/
|
1062
|
function _increase_count($entity_id, $number = 1) {
|
1063
|
db_merge('flag_counts')
|
1064
|
->key(array(
|
1065
|
'fid' => $this->fid,
|
1066
|
'entity_id' => $entity_id,
|
1067
|
))
|
1068
|
->fields(array(
|
1069
|
'entity_type' => $this->entity_type,
|
1070
|
'count' => $number,
|
1071
|
'last_updated' => REQUEST_TIME,
|
1072
|
))
|
1073
|
->updateFields(array(
|
1074
|
'last_updated' => REQUEST_TIME,
|
1075
|
))
|
1076
|
->expression('count', 'count + :inc', array(':inc' => $number))
|
1077
|
->execute();
|
1078
|
|
1079
|
// Reset the static cache of flag counts, so code running after this gets
|
1080
|
// correct counts.
|
1081
|
drupal_static_reset('flag_get_counts');
|
1082
|
drupal_static_reset('flag_get_flag_counts');
|
1083
|
}
|
1084
|
|
1085
|
/**
|
1086
|
* Decreases the flag count for an object and clears the static counts cache.
|
1087
|
*
|
1088
|
* @param $entity_id
|
1089
|
* For which item should the count be descreased.
|
1090
|
* @param $number
|
1091
|
* The amount of counts to decrease. Defaults to 1.
|
1092
|
*
|
1093
|
* @private
|
1094
|
*/
|
1095
|
function _decrease_count($entity_id, $number = 1) {
|
1096
|
// Delete rows with count 0, for data consistency and space-saving.
|
1097
|
// Done before the db_update() to prevent out-of-bounds errors on "count".
|
1098
|
db_delete('flag_counts')
|
1099
|
->condition('fid', $this->fid)
|
1100
|
->condition('entity_id', $entity_id)
|
1101
|
->condition('count', $number, '<=')
|
1102
|
->execute();
|
1103
|
|
1104
|
// Update the count with the new value otherwise.
|
1105
|
db_update('flag_counts')
|
1106
|
->expression('count', 'count - :inc', array(':inc' => $number))
|
1107
|
->fields(array(
|
1108
|
'last_updated' => REQUEST_TIME,
|
1109
|
))
|
1110
|
->condition('fid', $this->fid)
|
1111
|
->condition('entity_id', $entity_id)
|
1112
|
->execute();
|
1113
|
|
1114
|
// Reset the static cache of flag counts, so code running after this gets
|
1115
|
// correct counts.
|
1116
|
drupal_static_reset('flag_get_counts');
|
1117
|
drupal_static_reset('flag_get_flag_counts');
|
1118
|
}
|
1119
|
|
1120
|
/**
|
1121
|
* Set a cookie for anonymous users to record their flagging.
|
1122
|
*
|
1123
|
* @private
|
1124
|
*/
|
1125
|
function _flag_anonymous($entity_id) {
|
1126
|
$storage = FlagCookieStorage::factory($this);
|
1127
|
$storage->flag($entity_id);
|
1128
|
}
|
1129
|
|
1130
|
/**
|
1131
|
* Remove the cookie for anonymous users to record their unflagging.
|
1132
|
*
|
1133
|
* @private
|
1134
|
*/
|
1135
|
function _unflag_anonymous($entity_id) {
|
1136
|
$storage = FlagCookieStorage::factory($this);
|
1137
|
$storage->unflag($entity_id);
|
1138
|
}
|
1139
|
|
1140
|
/**
|
1141
|
* Returns the number of times an item is flagged.
|
1142
|
*
|
1143
|
* Thanks to using a cache, inquiring several different flags about the same
|
1144
|
* item results in only one SQL query.
|
1145
|
*/
|
1146
|
function get_count($entity_id) {
|
1147
|
$counts = flag_get_counts($this->entity_type, $entity_id);
|
1148
|
return isset($counts[$this->name]) ? $counts[$this->name] : 0;
|
1149
|
}
|
1150
|
|
1151
|
/**
|
1152
|
* Returns the number of items a user has flagged.
|
1153
|
*
|
1154
|
* For global flags, pass '0' as the user ID and session ID.
|
1155
|
*/
|
1156
|
function get_user_count($uid, $sid = NULL) {
|
1157
|
if (!isset($sid)) {
|
1158
|
$sid = flag_get_sid($uid);
|
1159
|
}
|
1160
|
return db_select('flagging', 'fc')->fields('fc', array('flagging_id'))
|
1161
|
->condition('fid', $this->fid)
|
1162
|
->condition('uid', $uid)
|
1163
|
->condition('sid', $sid)
|
1164
|
->countQuery()
|
1165
|
->execute()
|
1166
|
->fetchField();
|
1167
|
}
|
1168
|
|
1169
|
/**
|
1170
|
* Processes a flag label for display. This means language translation and
|
1171
|
* token replacements.
|
1172
|
*
|
1173
|
* You should always call this function and not get at the label directly.
|
1174
|
* E.g., do `print $flag->get_label('title')` instead of `print
|
1175
|
* $flag->title`.
|
1176
|
*
|
1177
|
* @param $label
|
1178
|
* The label to get, e.g. 'title', 'flag_short', 'unflag_short', etc.
|
1179
|
* @param $entity_id
|
1180
|
* The ID in whose context to interpret tokens. If not given, only global
|
1181
|
* tokens will be substituted.
|
1182
|
*
|
1183
|
* @return
|
1184
|
* The processed label.
|
1185
|
*/
|
1186
|
function get_label($label, $entity_id = NULL) {
|
1187
|
if (!isset($this->$label)) {
|
1188
|
return;
|
1189
|
}
|
1190
|
$label = t($this->$label);
|
1191
|
if (strpos($label, '[') !== FALSE) {
|
1192
|
$label = $this->replace_tokens($label, array(), array('sanitize' => FALSE), $entity_id);
|
1193
|
}
|
1194
|
return filter_xss_admin($label);
|
1195
|
}
|
1196
|
|
1197
|
/**
|
1198
|
* Get the link type for this flag.
|
1199
|
*/
|
1200
|
function get_link_type() {
|
1201
|
$link_types = flag_get_link_types();
|
1202
|
return (isset($this->link_type) && isset($link_types[$this->link_type])) ? $link_types[$this->link_type] : $link_types['normal'];
|
1203
|
}
|
1204
|
|
1205
|
/**
|
1206
|
* Replaces tokens in a label. Only the 'global' token context is recognized
|
1207
|
* by default, so derived classes should override this method to add all
|
1208
|
* token contexts they understand.
|
1209
|
*/
|
1210
|
function replace_tokens($label, $contexts, $options, $entity_id) {
|
1211
|
if (strpos($label , 'flagging:') !== FALSE) {
|
1212
|
if (($flagging = $this->get_flagging($entity_id))) {
|
1213
|
$contexts['flagging'] = $flagging;
|
1214
|
}
|
1215
|
}
|
1216
|
return token_replace($label, $contexts, $options);
|
1217
|
}
|
1218
|
|
1219
|
/**
|
1220
|
* Returns the token types this flag understands in labels. These are used
|
1221
|
* for narrowing down the token list shown in the help box to only the
|
1222
|
* relevant ones.
|
1223
|
*
|
1224
|
* Derived classes should override this.
|
1225
|
*/
|
1226
|
function get_labels_token_types() {
|
1227
|
return array('flagging');
|
1228
|
}
|
1229
|
|
1230
|
/**
|
1231
|
* A convenience method for getting the flag title.
|
1232
|
*
|
1233
|
* `$flag->get_title()` is shorthand for `$flag->get_label('title')`.
|
1234
|
*/
|
1235
|
function get_title($entity_id = NULL, $reset = FALSE) {
|
1236
|
static $titles = array();
|
1237
|
if ($reset) {
|
1238
|
$titles = array();
|
1239
|
}
|
1240
|
$slot = intval($entity_id); // Convert NULL to 0.
|
1241
|
if (!isset($titles[$this->fid][$slot])) {
|
1242
|
$titles[$this->fid][$slot] = $this->get_label('title', $entity_id);
|
1243
|
}
|
1244
|
return $titles[$this->fid][$slot];
|
1245
|
}
|
1246
|
|
1247
|
/**
|
1248
|
* Returns a 'flag action' object. It exists only for the sake of its
|
1249
|
* informative tokens. Currently, it's utilized only for the 'mail' action.
|
1250
|
*
|
1251
|
* Derived classes should populate the 'content_title' and 'content_url'
|
1252
|
* slots.
|
1253
|
*/
|
1254
|
function get_flag_action($entity_id) {
|
1255
|
$flag_action = new stdClass();
|
1256
|
$flag_action->flag = $this->name;
|
1257
|
$flag_action->entity_type = $this->entity_type;
|
1258
|
$flag_action->entity_id = $entity_id;
|
1259
|
return $flag_action;
|
1260
|
}
|
1261
|
|
1262
|
/**
|
1263
|
* Returns an array of errors set during validation.
|
1264
|
*/
|
1265
|
function get_errors() {
|
1266
|
return $this->errors;
|
1267
|
}
|
1268
|
|
1269
|
/**
|
1270
|
* @addtogroup actions
|
1271
|
* @{
|
1272
|
* Methods that can be overridden to support Actions.
|
1273
|
*/
|
1274
|
|
1275
|
/**
|
1276
|
* Returns an array of all actions that are executable with this flag.
|
1277
|
*/
|
1278
|
function get_valid_actions() {
|
1279
|
$actions = module_invoke_all('action_info');
|
1280
|
foreach ($actions as $callback => $action) {
|
1281
|
if ($action['type'] != $this->entity_type && !in_array('any', $action['triggers'])) {
|
1282
|
unset($actions[$callback]);
|
1283
|
}
|
1284
|
}
|
1285
|
return $actions;
|
1286
|
}
|
1287
|
|
1288
|
/**
|
1289
|
* Returns objects the action may possibly need. This method should return at
|
1290
|
* least the 'primary' object the action operates on.
|
1291
|
*
|
1292
|
* This method is needed because get_valid_actions() returns actions that
|
1293
|
* don't necessarily operate on an object of a type this flag manages. For
|
1294
|
* example, flagging a comment may trigger an 'Unpublish post' action on a
|
1295
|
* node; So the comment flag needs to tell the action about some node.
|
1296
|
*
|
1297
|
* Derived classes must implement this.
|
1298
|
*
|
1299
|
* @abstract
|
1300
|
*/
|
1301
|
function get_relevant_action_objects($entity_id) {
|
1302
|
return array();
|
1303
|
}
|
1304
|
|
1305
|
/**
|
1306
|
* @} End of "addtogroup actions".
|
1307
|
*/
|
1308
|
|
1309
|
/**
|
1310
|
* @addtogroup views
|
1311
|
* @{
|
1312
|
* Methods that can be overridden to support the Views module.
|
1313
|
*/
|
1314
|
|
1315
|
/**
|
1316
|
* Returns information needed for Views integration. E.g., the Views table
|
1317
|
* holding the flagged object, its primary key, and various labels. See
|
1318
|
* derived classes for examples.
|
1319
|
*
|
1320
|
* @static
|
1321
|
*/
|
1322
|
function get_views_info() {
|
1323
|
return array();
|
1324
|
}
|
1325
|
|
1326
|
/**
|
1327
|
* @} End of "addtogroup views".
|
1328
|
*/
|
1329
|
|
1330
|
/**
|
1331
|
* Saves a flag to the database. It is a wrapper around update() and insert().
|
1332
|
*/
|
1333
|
function save() {
|
1334
|
// Allow the 'global' property to be a boolean, particularly when defined in
|
1335
|
// hook_flag_default_flags(). Without this, a value of FALSE gets casted to
|
1336
|
// an empty string which violates our schema. Other boolean properties are
|
1337
|
// fine, as they are serialized.
|
1338
|
$this->global = (int) $this->global;
|
1339
|
|
1340
|
if (isset($this->fid)) {
|
1341
|
$this->update();
|
1342
|
$this->is_new = FALSE;
|
1343
|
}
|
1344
|
else {
|
1345
|
$this->insert();
|
1346
|
$this->is_new = TRUE;
|
1347
|
}
|
1348
|
// Clear the page cache for anonymous users.
|
1349
|
cache_clear_all('*', 'cache_page', TRUE);
|
1350
|
}
|
1351
|
|
1352
|
/**
|
1353
|
* Saves an existing flag to the database. Better use save().
|
1354
|
*/
|
1355
|
function update() {
|
1356
|
db_update('flag')->fields(array(
|
1357
|
'name' => $this->name,
|
1358
|
'title' => $this->title,
|
1359
|
'global' => $this->global,
|
1360
|
'options' => $this->get_serialized_options()))
|
1361
|
->condition('fid', $this->fid)
|
1362
|
->execute();
|
1363
|
db_delete('flag_types')->condition('fid', $this->fid)->execute();
|
1364
|
foreach ($this->types as $type) {
|
1365
|
db_insert('flag_types')->fields(array(
|
1366
|
'fid' => $this->fid,
|
1367
|
'type' => $type))
|
1368
|
->execute();
|
1369
|
}
|
1370
|
}
|
1371
|
|
1372
|
/**
|
1373
|
* Saves a new flag to the database. Better use save().
|
1374
|
*/
|
1375
|
function insert() {
|
1376
|
$this->fid = db_insert('flag')
|
1377
|
->fields(array(
|
1378
|
'entity_type' => $this->entity_type,
|
1379
|
'name' => $this->name,
|
1380
|
'title' => $this->title,
|
1381
|
'global' => $this->global,
|
1382
|
'options' => $this->get_serialized_options(),
|
1383
|
))
|
1384
|
->execute();
|
1385
|
foreach ($this->types as $type) {
|
1386
|
db_insert('flag_types')
|
1387
|
->fields(array(
|
1388
|
'fid' => $this->fid,
|
1389
|
'type' => $type,
|
1390
|
))
|
1391
|
->execute();
|
1392
|
}
|
1393
|
}
|
1394
|
|
1395
|
/**
|
1396
|
* Options are stored serialized in the database.
|
1397
|
*/
|
1398
|
function get_serialized_options() {
|
1399
|
$option_names = array_keys($this->options());
|
1400
|
$options = array();
|
1401
|
foreach ($option_names as $option) {
|
1402
|
$options[$option] = $this->$option;
|
1403
|
}
|
1404
|
return serialize($options);
|
1405
|
}
|
1406
|
|
1407
|
/**
|
1408
|
* Deletes a flag from the database.
|
1409
|
*/
|
1410
|
function delete() {
|
1411
|
db_delete('flag')->condition('fid', $this->fid)->execute();
|
1412
|
db_delete('flagging')->condition('fid', $this->fid)->execute();
|
1413
|
db_delete('flag_types')->condition('fid', $this->fid)->execute();
|
1414
|
db_delete('flag_counts')->condition('fid', $this->fid)->execute();
|
1415
|
module_invoke_all('flag_delete', $this);
|
1416
|
}
|
1417
|
|
1418
|
/**
|
1419
|
* Returns TRUE if this flag's declared API version is compatible with this
|
1420
|
* module.
|
1421
|
*
|
1422
|
* An "incompatible" flag is one exported (and now being imported or exposed
|
1423
|
* via hook_flag_default_flags()) by a different version of the Flag module.
|
1424
|
* An incompatible flag should be treated as a "black box": it should not be
|
1425
|
* saved or exported because our code may not know to handle its internal
|
1426
|
* structure.
|
1427
|
*/
|
1428
|
function is_compatible() {
|
1429
|
if (isset($this->fid)) {
|
1430
|
// Database flags are always compatible.
|
1431
|
return TRUE;
|
1432
|
}
|
1433
|
else {
|
1434
|
if (!isset($this->api_version)) {
|
1435
|
$this->api_version = 1;
|
1436
|
}
|
1437
|
return $this->api_version == FLAG_API_VERSION;
|
1438
|
}
|
1439
|
}
|
1440
|
|
1441
|
/**
|
1442
|
* Finds the "default flag" corresponding to this flag.
|
1443
|
*
|
1444
|
* Flags defined in code ("default flags") can be overridden. This method
|
1445
|
* returns the default flag that is being overridden by $this. Returns NULL
|
1446
|
* if $this overrides no default flag.
|
1447
|
*/
|
1448
|
function find_default_flag() {
|
1449
|
if ($this->fid) {
|
1450
|
$default_flags = flag_get_default_flags(TRUE);
|
1451
|
if (isset($default_flags[$this->name])) {
|
1452
|
return $default_flags[$this->name];
|
1453
|
}
|
1454
|
}
|
1455
|
}
|
1456
|
|
1457
|
/**
|
1458
|
* Reverts an overriding flag to its default state.
|
1459
|
*
|
1460
|
* Note that $this isn't altered. To see the reverted flag you'll have to
|
1461
|
* call flag_get_flag($this->name) again.
|
1462
|
*
|
1463
|
* @return
|
1464
|
* TRUE if the flag was reverted successfully; FALSE if there was an error;
|
1465
|
* NULL if this flag overrides no default flag.
|
1466
|
*/
|
1467
|
function revert() {
|
1468
|
if (($default_flag = $this->find_default_flag())) {
|
1469
|
if ($default_flag->is_compatible()) {
|
1470
|
$default_flag = clone $default_flag;
|
1471
|
$default_flag->fid = $this->fid;
|
1472
|
$default_flag->save();
|
1473
|
drupal_static_reset('flag_get_flags');
|
1474
|
return TRUE;
|
1475
|
}
|
1476
|
else {
|
1477
|
return FALSE;
|
1478
|
}
|
1479
|
}
|
1480
|
}
|
1481
|
|
1482
|
/**
|
1483
|
* Disable a flag provided by a module.
|
1484
|
*/
|
1485
|
function disable() {
|
1486
|
if (isset($this->module)) {
|
1487
|
$flag_status = variable_get('flag_default_flag_status', array());
|
1488
|
$flag_status[$this->name] = FALSE;
|
1489
|
variable_set('flag_default_flag_status', $flag_status);
|
1490
|
}
|
1491
|
}
|
1492
|
|
1493
|
/**
|
1494
|
* Enable a flag provided by a module.
|
1495
|
*/
|
1496
|
function enable() {
|
1497
|
if (isset($this->module)) {
|
1498
|
$flag_status = variable_get('flag_default_flag_status', array());
|
1499
|
$flag_status[$this->name] = TRUE;
|
1500
|
variable_set('flag_default_flag_status', $flag_status);
|
1501
|
}
|
1502
|
}
|
1503
|
|
1504
|
/**
|
1505
|
* Returns administrative menu path for carrying out some action.
|
1506
|
*/
|
1507
|
function admin_path($action) {
|
1508
|
if ($action == 'edit') {
|
1509
|
// Since 'edit' is the default tab, we omit the action.
|
1510
|
return FLAG_ADMIN_PATH . '/manage/' . $this->name;
|
1511
|
}
|
1512
|
else {
|
1513
|
return FLAG_ADMIN_PATH . '/manage/' . $this->name . '/' . $action;
|
1514
|
}
|
1515
|
}
|
1516
|
|
1517
|
/**
|
1518
|
* Renders a flag/unflag link.
|
1519
|
*
|
1520
|
* This is a wrapper around theme('flag') that channels the call to the right
|
1521
|
* template file.
|
1522
|
*
|
1523
|
* @param $action
|
1524
|
* The action the link is about to carry out, either "flag" or "unflag".
|
1525
|
* @param $entity_id
|
1526
|
* The ID of the object to flag.
|
1527
|
* @param $variables = array()
|
1528
|
* An array of further variables to pass to theme('flag'). For the full list
|
1529
|
* of parameters, see flag.tpl.php. Of particular interest:
|
1530
|
* - after_flagging: Set to TRUE if this flag link is being displayed as the
|
1531
|
* result of a flagging action.
|
1532
|
* - errors: An array of error messages.
|
1533
|
*
|
1534
|
* @return
|
1535
|
* The HTML for the flag link.
|
1536
|
*/
|
1537
|
function theme($action, $entity_id, $variables = array()) {
|
1538
|
static $js_added = array();
|
1539
|
global $user;
|
1540
|
|
1541
|
$after_flagging = !empty($variables['after_flagging']);
|
1542
|
|
1543
|
// If the flagging user is anonymous, set a boolean for the benefit of
|
1544
|
// JavaScript code. Currently, only our "anti-crawlers" mechanism uses it.
|
1545
|
if ($user->uid == 0 && !isset($js_added['anonymous'])) {
|
1546
|
$js_added['anonymous'] = TRUE;
|
1547
|
drupal_add_js(array('flag' => array('anonymous' => TRUE)), 'setting');
|
1548
|
}
|
1549
|
|
1550
|
// If the flagging user is anonymous and the page cache is enabled, we
|
1551
|
// update the links through JavaScript.
|
1552
|
if ($this->uses_anonymous_cookies() && !$after_flagging) {
|
1553
|
if ($this->global) {
|
1554
|
// In case of global flags, the JavaScript template is to contain
|
1555
|
// the opposite of the current state.
|
1556
|
$js_action = ($action == 'flag' ? 'unflag' : 'flag');
|
1557
|
}
|
1558
|
else {
|
1559
|
// In case of non-global flags, we always show the "flag!" link,
|
1560
|
// and then replace it with the "unflag!" link through JavaScript.
|
1561
|
$js_action = 'unflag';
|
1562
|
$action = 'flag';
|
1563
|
}
|
1564
|
if (!isset($js_added[$this->name . '_' . $entity_id])) {
|
1565
|
$js_added[$this->name . '_' . $entity_id] = TRUE;
|
1566
|
$js_template = theme($this->theme_suggestions(), array(
|
1567
|
'flag' => $this,
|
1568
|
'action' => $js_action,
|
1569
|
'entity_id' => $entity_id,
|
1570
|
'after_flagging' => $after_flagging,
|
1571
|
));
|
1572
|
drupal_add_js(array('flag' => array('templates' => array($this->name . '_' . $entity_id => $js_template))), 'setting');
|
1573
|
}
|
1574
|
}
|
1575
|
|
1576
|
return theme($this->theme_suggestions(), array(
|
1577
|
'flag' => $this,
|
1578
|
'action' => $action,
|
1579
|
'entity_id' => $entity_id,
|
1580
|
) + $variables);
|
1581
|
}
|
1582
|
|
1583
|
/**
|
1584
|
* Provides an array of possible themes to try for a given flag.
|
1585
|
*/
|
1586
|
function theme_suggestions() {
|
1587
|
$suggestions = array();
|
1588
|
$suggestions[] = 'flag__' . $this->name;
|
1589
|
$suggestions[] = 'flag__' . $this->link_type;
|
1590
|
$suggestions[] = 'flag';
|
1591
|
return $suggestions;
|
1592
|
}
|
1593
|
|
1594
|
/**
|
1595
|
* A shortcut function to output the link URL.
|
1596
|
*/
|
1597
|
function _flag_url($path, $fragment = NULL, $absolute = TRUE) {
|
1598
|
return url($path, array('fragment' => $fragment, 'absolute' => $absolute));
|
1599
|
}
|
1600
|
}
|
1601
|
|
1602
|
/**
|
1603
|
* A dummy flag to be used where the real implementation can't be found.
|
1604
|
*/
|
1605
|
class flag_broken extends flag_flag {
|
1606
|
function options_form(&$form) {
|
1607
|
drupal_set_message(t("The module providing this flag wasn't found, or this flag type, %type, isn't valid.", array('%type' => $this->entity_type)), 'error');
|
1608
|
$form = array();
|
1609
|
}
|
1610
|
}
|