-
Notifications
You must be signed in to change notification settings - Fork 0
/
tac_lite.module
335 lines (317 loc) · 13.1 KB
/
tac_lite.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
<?php
/**
* @file
* Control access to site content based on taxonomy, roles and users.
*/
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Renderer;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Drupal\tac_lite\Form\SchemeForm;
use Drupal\taxonomy\Entity\Term;
/**
* Implements hook_help().
*/
function tac_lite_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the block module.
case 'help.page.tac_lite':
$admin_taclite = Url::fromRoute('tac_lite.administration');
$admin_tac_lite_url = Link::fromTextAndUrl(t('administer -> people -> access control by taxonomy'), $admin_taclite);
$output = '<p>' . t('Taxonomy Access Control Lite allows you to restrict access to site content. It uses a simple scheme based on Taxonomy, Users and Roles.') . '</p>';
$output .= '<p>' . t("This module leverages Drupal's node_access table allows this module to grant permission to view, update, and delete nodes. To control which users can <em>create</em> new nodes, use Drupal's role based permissions.") . '</p>';
$output .= '<p>' . t("It is important to understand that this module <em>grants</em> privileges, as opposed to <em>revoking</em> privileges. So, use Drupal's built-in permissions to hide content from certain roles, then use this module to show the content. This module cannot hide content that the user is allowed to see based on their existing privileges.") . '</p>';
$output .= '<p>' . t('There are several steps required to set up Taxonomy Access Control Lite.') . '</p>';
$output .= '<ol>';
$output .= '<li>' . t("Define one or more vocabularies whose terms will control which users have access. For example, you could define a vocabulary called 'Privacy' with terms 'Public' and 'Private'.") . '</li>';
$output .= '<li>' . t('Tell this module which vocabularies control privacy :url', [
':url' => $admin_tac_lite_url,
]) . '</li>';
$output .= '<li>' . t('Configure one or more <em>schemes</em>. simple site may need only one scheme which grants view permission. A more complex site might require additional schemes for update and delete. Each scheme associates roles and terms. Users will be granted priviliges based on their role and the terms with which nodes are tagged.') . '</li>';
$output .= '<li>' . t('When settings are correct, <a href=:url>rebuild node_access permissions</a>.', [
':url' => Url::fromRoute('node.configure_rebuild_confirm')->toString(),
]) . '</li>';
$output .= '<li>' . t('Optionally, grant access to individual users. (See the <em>access by taxonomy</em> tab, under user -> edit.)') . '</li>';
$output .= '</ol>';
$output .= '<p>' . t('Troubleshooting:.') . '<ul>';
$output .= '<li>' . t('Try rebuilding node_access permissions.') . '</li>';
$output .= '<li>' . t('Try disabling tac_lite.module, rebuilding permissions. With the module disabled, users should not have the privileges you are attempting to grant with this module.') . '</li>';
$output .= '<li>' . t("The devel_node_access.module (part of <a href=:url>devel</a>) helps to see exactly what Drupal's node_access table is doing.", [
':url' => 'http://drupal.org/project/devel',
]) . '</li>';
$output .= '</ul></p>';
return $output;
}
}
/**
* Implements hook_node_access_records().
*/
function tac_lite_node_access_records(NodeInterface $node) {
$tids = _tac_lite_get_terms($node);
if (count($tids)) {
// If we're here, the node has terms associated with it which restrict
// access to the node.
$grants = [];
$settings = \Drupal::config('tac_lite.settings');
$schemes = $settings->get('tac_lite_schemes');
for ($i = 1; $i <= $schemes; $i++) {
$config = SchemeForm::tacLiteConfig($i);
// Only apply grants to published nodes, or unpublished nodes
// if requested in the scheme.
if ($node->isPublished() || $config['unpublished']) {
foreach ($tids as $tid) {
$grant = [
'realm' => $config['realm'],
// Use term id as grant id.
'gid' => $tid,
'grant_view' => 0,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
foreach ($config['perms'] as $perm) {
$grant[$perm] = 1;
}
$grants[] = $grant;
}
}
}
return $grants;
}
}
/**
* Gets terms from a node that belong to vocabularies selected.
*/
function _tac_lite_get_terms($node) {
$tids = [];
// Get the vids that tac_lite cares about.
$config = \Drupal::config('tac_lite.settings');
$vids = $config->get('tac_lite_categories') ? array_keys($config->get('tac_lite_categories')) : NULL;
if ($vids) {
// Load all terms found in term reference fields.
// This logic should work for all nodes (published or not).
$terms_by_vid = tac_lite_node_get_terms($node);
if (!empty($terms_by_vid)) {
foreach ($vids as $vid) {
if (!empty($terms_by_vid[$vid])) {
foreach ($terms_by_vid[$vid] as $tid => $term) {
$tids[$tid] = $tid;
}
}
}
}
}
elseif (\Drupal::currentUser()->hasPermission('administer tac_lite')) {
\Drupal::messenger()->addStatus(t('tac_lite.module enabled, but not <a href=:admin_url>configured</a>. No tac_lite terms associated with :title.', [
':admin_url' => Url::fromRoute('tac_lite.administration')->toString(),
':title' => $node->getTitle(),
]
));
}
return $tids;
}
/**
* We organize our data structure by vid and tid.
*/
function tac_lite_node_get_terms($node) {
$terms = &drupal_static(__FUNCTION__);
$nid = $node->id();
if (!isset($terms[$nid])) {
// Get fields of all node.
$fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', $node->getType());
// Get tids from all taxonomy_term_reference fields.
foreach ($fields as $field_name => $field) {
$field_type = method_exists($field, 'getType') ? $field->getType() : NULL;
$target_type = method_exists($field, 'getSetting') ? $field->getSetting('target_type') : NULL;
// Get all terms, regardless of language, associated with the node.
if ($field_type == 'entity_reference' && $target_type == 'taxonomy_term') {
$field_name = $field->get('field_name');
if ($items = $node->get($field_name)->getValue()) {
foreach ($items as $item) {
// We need to term to determine the vocabulary id.
if (!empty($item['target_id'])) {
$term = Term::load($item['target_id']);
}
if ($term) {
$terms[$node->id()][$term->bundle()][$term->id()] = $term;
}
}
}
}
}
}
return isset($terms[$node->id()]) ? $terms[$node->id()] : FALSE;
}
/**
* Implements hook_node_grants().
*
* Returns any grants which may give the user permission to perform the
* requested op.
*/
function tac_lite_node_grants(AccountInterface $account, $op) {
$grants = [];
$settings = \Drupal::config('tac_lite.settings');
$schemes = $settings->get('tac_lite_schemes');
for ($i = 1; $i <= $schemes; $i++) {
$config = SchemeForm::tacLiteConfig($i);
if (in_array('grant_' . $op, $config['perms'])) {
$grants[$config['realm']] = _tac_lite_user_tids($account, $i, $config);
}
}
if (count($grants)) {
return $grants;
}
}
/**
* Return the term ids of terms this user is allowed to access.
*
* Users are granted access to terms either because of who they are,
* or because of the roles they have.
*/
function _tac_lite_user_tids($account, $scheme) {
// Grant id 0 is reserved for nodes which were not given a grant id
// when they were created. By adding 0 to the grant id, we let the
// user view those nodes.
$grants = [0];
$data = \Drupal::service('user.data')->get('tac_lite', $account->id(), 'tac_lite_scheme_' . $scheme) ?: [];
if (count($data)) {
foreach ($data as $tids) {
if (count($tids)) {
$grants = array_merge($grants, $tids);
}
}
}
// Add per-role grants in addition to per-user grants.
$settings = \Drupal::config('tac_lite.settings');
$defaults = $settings->get('tac_lite_grants_scheme_' . $scheme);
$defaults = $defaults ? $defaults : [];
$roles = $account->getRoles();
foreach ($roles as $rid) {
if (isset($defaults[$rid]) && count($defaults[$rid])) {
foreach ($defaults[$rid] as $tids) {
if (count($tids)) {
$grants = array_merge($grants, $tids);
}
}
}
}
// Remove any duplicates.
$grants = array_unique($grants);
// Because of some flakiness in the form API and the form we insert under
// user settings, we may have a bogus entry with vid set
// to ''. Here we make sure not to return that.
unset($grants['']);
return $grants;
}
/**
* Implements hook_query_TAG_alter().
*/
function tac_lite_query_term_access_alter(AlterableInterface $query) {
$account = \Drupal::currentUser();
// If this user has administer rights, don't filter.
if (\Drupal::currentUser()->hasPermission('administer tac_lite')) {
return;
}
// Get our vocabularies and schemes from variables. Return if we have none.
$settings = \Drupal::config('tac_lite.settings');
$vids = $settings->get('tac_lite_categories');
$schemes = $settings->get('tac_lite_schemes');
if (!$vids || !count($vids) || !$schemes) {
return;
}
// The terms this user is allowed to see.
$term_visibility = FALSE;
$tids = [];
$visibility_scheme_ids = [];
for ($i = 1; $i <= $schemes; $i++) {
$config = SchemeForm::tacLiteConfig($i);
if ($config['term_visibility']) {
$tids = array_merge($tids, _tac_lite_user_tids($account, $i));
$term_visibility = TRUE;
$visibility_scheme_ids[] = $i;
}
}
if ($term_visibility) {
// HELP: What is the proper way to find the alias of the primary table here?
$primary_table = '';
$t = $query->getTables();
foreach ($t as $info) {
$table = $info['table'];
if ($table == 'taxonomy_term_data' || $table == 'taxonomy_term_field_data') {
$primary_table = $info['alias'];
}
}
// Prevent query from finding terms the current user does
// not have permission to see.
$query->leftJoin('taxonomy_term_field_data', 'tac_td', $primary_table . '.tid = tac_td.tid');
$or = new Condition('OR');
$or->condition($primary_table . '.tid', $tids, 'IN');
$or->condition('tac_td.vid', $vids, 'NOT IN');
$query->condition($or);
_tac_lite_apply_cache_contexts_to_render_context($visibility_scheme_ids);
}
}
/**
* Bubbles-up appropriate TAC-Lite cache contexts to the current render context.
*
* This ensures that the current render context has a
* 'user.tac_lite_grants:$scheme_id' cache context.
*
* (This pattern was adapted from changes made to node_query_node_access_alter()
* in #2557815 as a backstop to ensure that any render context that includes
* taxonomy terms affected by TAC Lite is invalidated when a user's TAC Lite
* permissions change).
*
* @param int[] $scheme_ids
* The IDs of the TAC Lite schemes for which a cache context needs to be
* added to the render context.
*
* @throws \Exception
* If the renderer fails to render with the added cache contexts.
*/
function _tac_lite_apply_cache_contexts_to_render_context(array $scheme_ids) {
$request = \Drupal::requestStack()->getCurrentRequest();
$renderer = \Drupal::service('renderer');
assert($renderer instanceof Renderer);
if ($request->isMethodCacheable() && $renderer->hasRenderContext()) {
$cache_contexts = array_map(
function ($scheme_id) {
return 'user.tac_lite_grants:' . $scheme_id;
},
$scheme_ids
);
$build = [
'#cache' => ['contexts' => $cache_contexts],
];
$renderer->render($build);
}
}
/**
* Implements hook_entity_type_alter().
*
* @noinspection PhpUnused
*/
function tac_lite_entity_type_alter(array &$entity_types) {
$term_entity_type = $entity_types['taxonomy_term'] ?? NULL;
if ($term_entity_type !== NULL) {
assert($term_entity_type instanceof ContentEntityTypeInterface);
// Make listings of Taxonomy Terms aware of the TAC Lite grant cache
// context. This is used by listing controllers and Views to ensure that
// rendered listings are cached separately for different combinations of
// user-level TAC Lite grants, so two users with different grants, or the
// same user with different grants at two different points in time, end up
// with different results.
//
// See \Drupal\views\Plugin\views\cache\CachePluginBase::getCacheTags()
$list_contexts = $term_entity_type->getListCacheContexts();
$list_contexts[] = 'user.tac_lite_grants';
// Content entities don't provide a way for us to add items to the list, so
// we have to replace it in its entirety.
$term_entity_type->set('list_cache_contexts', array_unique($list_contexts));
}
}