taxonomy_access.create.inc 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873
  1. <?php
  2. /**
  3. * @file
  4. * Implements the Add Tag (create) grant on editing forms.
  5. *
  6. * These functions need to be included in three circumstances:
  7. * - Form building for forms with taxonomy fields.
  8. * - taxonomy_access_field_widget_form_alter()
  9. * - taxonomy_access_field_widget_taxonomy_autocomplete_form_alter()
  10. * - Form validation for forms with taxonomy fields.
  11. * - taxonomy_access_autocomplete_validate()
  12. * - taxonomy_access_options_validate()
  13. * - taxonomy_access_field_attach_validate()
  14. * - Taxonomy autocomplete AJAX requests.
  15. * - taxonomy_access_autocomplete()
  16. */
  17. /**
  18. * @defgroup tac_create Taxonomy Access Control: Add tag (create) permission
  19. * @{
  20. * Implement access control for taxonomy terms on node editing forms.
  21. */
  22. /**
  23. * Implements the create grant for autocomplete fields.
  24. *
  25. * - Denies access if the user cannot alter the field values.
  26. * - Determines whether the user can autocreate new terms for the field.
  27. * - Removes default values disallowed by create.
  28. * - Adds information on autocreate and disallowed defaults to the element so
  29. * it is available to the validator.
  30. * - Adds the custom validator.
  31. * - Sets a custom autocomplete path to filter autocomplete by create.
  32. *
  33. * Some of the logic here is borrowed from taxonomy_autocomplete_validate().
  34. *
  35. * @see taxonomy_access_field_widget_taxonomy_autocomplete_form_alter()
  36. */
  37. function _taxonomy_access_autocomplete_alter(&$element, &$form_state, $context) {
  38. // Collect a list of terms and filter out those disallowed by create.
  39. $filtered = array();
  40. foreach ($context['items'] as $item) {
  41. $filtered[$item['tid']] = $item;
  42. }
  43. $disallowed_defaults = taxonomy_access_create_disallowed(array_keys($filtered));
  44. foreach ($disallowed_defaults as $tid) {
  45. unset($filtered[$tid]);
  46. }
  47. // Assemble a list of all vocabularies for the field.
  48. $vids = array();
  49. foreach ($context['field']['settings']['allowed_values'] as $tree) {
  50. if ($vocab = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
  51. $vids[] = $vocab->vid;
  52. }
  53. }
  54. // Determine whether the user has create for any terms in the given vocabs.
  55. $allowed_terms = FALSE;
  56. foreach ($vids as $vid) {
  57. $terms = taxonomy_access_user_create_terms_by_vocab($vid);
  58. if (!empty($terms)) {
  59. $allowed_terms = TRUE;
  60. break;
  61. }
  62. }
  63. // Filter the vids to vocabs in which the user may create new terms.
  64. $allowed_vids = taxonomy_access_create_default_allowed($vids);
  65. // If the field already has the maximum number of values, and all of these
  66. // values are disallowed, deny access to the field.
  67. if ($context['field']['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
  68. if (sizeof($disallowed_defaults) >= $context['field']['cardinality']) {
  69. $element['#access'] = FALSE;
  70. }
  71. }
  72. // If the user may not create any terms on this field, deny access.
  73. if (empty($allowed_vids) && !$allowed_terms) {
  74. $element['#access'] = FALSE;
  75. }
  76. // Set the default value from the filtered item list.
  77. $element['#default_value'] =
  78. taxonomy_access_autocomplete_default_value($filtered);
  79. // Custom validation. Set values for the validator indicating:
  80. // 1. Whether the user can autocreate terms in this field (vocab. default).
  81. // 2. Which tids were removed due to create restrictions.
  82. $element['#allow_autocreate'] = empty($allowed_vids) ? FALSE : TRUE;
  83. $element['#disallowed_defaults'] = $disallowed_defaults;
  84. $element['#element_validate'] =
  85. array('taxonomy_access_autocomplete_validate');
  86. // Use a custom autocomplete path to filter by create rather than list.
  87. $element['#autocomplete_path'] =
  88. 'taxonomy_access/autocomplete/' . $context['field']['field_name'];
  89. unset($context);
  90. }
  91. /**
  92. * Implements the create grant for options widgets.
  93. *
  94. * - Denies access if the user cannot alter the field values.
  95. * - Attaches jQuery to disable values disallowed by create.
  96. * - Adds the disallowed values from the element so they are available to the
  97. * custom validator.
  98. * - Adds the custom validator.
  99. *
  100. * We use jQuery to disable the options because of FAPI limitations:
  101. * @see http://drupal.org/node/284917
  102. * @see http://drupal.org/node/342316
  103. * @see http://drupal.org/node/12089
  104. *
  105. * @see taxonomy_access_field_widget_form_alter()
  106. */
  107. function _taxonomy_access_options_alter(&$element, &$form_state, $context) {
  108. // Check for an existing entity ID.
  109. $entity_id = 0;
  110. if (!empty($form_state['build_info']['args'][0])) {
  111. $info = entity_get_info($context['instance']['entity_type']);
  112. $pseudo_entity = (object) $form_state['build_info']['args'][0];
  113. if (isset($pseudo_entity->{$info['entity keys']['id']})) {
  114. $entity_id = $pseudo_entity->{$info['entity keys']['id']};
  115. }
  116. }
  117. // Collect a list of terms and determine which are allowed
  118. $tids = array_keys($element['#options']);
  119. // Ignore the "none" option if present.
  120. $key = array_search('_none', $tids);
  121. if ($key !== FALSE) {
  122. unset($tids[$key]);
  123. }
  124. $allowed_tids = taxonomy_access_create_allowed($tids);
  125. $disallowed_tids = taxonomy_access_create_disallowed($tids);
  126. // If no options are allowed, deny access to the field.
  127. if (empty($allowed_tids)) {
  128. $element['#access'] = FALSE;
  129. }
  130. // On node creation, simply remove disallowed default values.
  131. if (!$entity_id) {
  132. $disallowed_defaults = array();
  133. if (is_array($element['#default_value'])) {
  134. foreach ($element['#default_value'] as $key => $value) {
  135. if (in_array($value, $disallowed_tids)) {
  136. unset($element['#default_value'][0]);
  137. }
  138. }
  139. }
  140. elseif (in_array($element['#default_value'], $disallowed_tids)) {
  141. unset($element['#default_value']);
  142. }
  143. }
  144. // If the node already exists, check:
  145. // 1. Whether the field already has the maximum number of values
  146. // 2. Whether all of these values are disallowed.
  147. // If both these things are true, then the user cannot edit the field's
  148. // value, so disallow access.
  149. else {
  150. $defaults =
  151. is_array($element['#default_value'])
  152. ? $element['#default_value']
  153. : array($element['#default_value'])
  154. ;
  155. $disallowed_defaults =
  156. array_intersect($defaults, $disallowed_tids);
  157. if ($context['field']['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
  158. if (sizeof($disallowed_defaults) >= $context['field']['cardinality']) {
  159. $element['#access'] = FALSE;
  160. }
  161. }
  162. }
  163. // If there are disallowed, terms, add CSS and JS for jQuery.
  164. // We use jQuery because FAPI does not currently support attributes
  165. // for individual options.
  166. if (!empty($disallowed_tids)) {
  167. // Add a css class to the field that we can use in jQuery.
  168. $class_name = 'tac_' . $element['#field_name'];
  169. $element['#attributes']['class'][] = $class_name;
  170. // Add js for disabling create options.
  171. $settings[] = array(
  172. 'field' => $class_name,
  173. 'disallowed_tids' => $disallowed_tids,
  174. 'disallowed_defaults' => $disallowed_defaults,
  175. );
  176. $element['#attached']['js'][] =
  177. drupal_get_path('module', 'taxonomy_access') . '/tac_create.js';
  178. $element['#attached']['js'][] = array(
  179. 'data' => array('taxonomy_access' => $settings),
  180. 'type' => 'setting',
  181. );
  182. }
  183. $element['#disallowed_defaults'] = $disallowed_defaults;
  184. $element['#element_validate'] = array('taxonomy_access_options_validate');
  185. }
  186. /**
  187. * Retrieve terms that the current user may create.
  188. *
  189. * @return array|true
  190. * An array of term IDs, or TRUE if the user may create all terms.
  191. *
  192. * @see taxonomy_access_user_create_terms_by_vocab()
  193. * @see _taxonomy_access_user_term_grants()
  194. */
  195. function taxonomy_access_user_create_terms() {
  196. // Cache the terms the current user can create.
  197. $terms = &drupal_static(__FUNCTION__, NULL);
  198. if (is_null($terms)) {
  199. $terms = _taxonomy_access_user_term_grants(TRUE);
  200. }
  201. return $terms;
  202. }
  203. /**
  204. * Retrieve terms that the current user may create in specific vocabularies.
  205. *
  206. * @param int $vid
  207. * A vid to use as a filter.
  208. *
  209. * @return array|true
  210. * An array of term IDs, or TRUE if the user may create all terms.
  211. *
  212. * @see taxonomy_access_user_create_terms()
  213. * @see _taxonomy_access_user_term_grants()
  214. */
  215. function taxonomy_access_user_create_terms_by_vocab($vid) {
  216. // Cache the terms the current user can create per vocabulary.
  217. static $terms = array();
  218. if (!isset($terms[$vid])) {
  219. $terms[$vid] = _taxonomy_access_user_term_grants(TRUE, array($vid));
  220. }
  221. return $terms[$vid];
  222. }
  223. /**
  224. * Retrieve terms that the current user may create.
  225. *
  226. * @return array|true
  227. * An array of term IDs, or TRUE if the user may create all terms.
  228. *
  229. * @see _taxonomy_access_create_defaults()
  230. */
  231. function taxonomy_access_user_create_defaults() {
  232. // Cache the terms the current user can create.
  233. static $vids = NULL;
  234. if (is_null($vids)) {
  235. $vids = _taxonomy_access_create_defaults();
  236. }
  237. return $vids;
  238. }
  239. /**
  240. * Check a list of term IDs for terms the user may not create.
  241. *
  242. * @param array $tids
  243. * The array of term IDs.
  244. *
  245. * @return array
  246. * An array of disallowed term IDs.
  247. */
  248. function taxonomy_access_create_disallowed(array $tids) {
  249. $all_allowed = taxonomy_access_user_create_terms();
  250. // If the user's create grant info is exactly TRUE, no terms are disallowed.
  251. if ($all_allowed === TRUE) {
  252. return array();
  253. }
  254. return array_diff($tids, $all_allowed);
  255. }
  256. /**
  257. * Filter a list of term IDs to terms the user may create.
  258. *
  259. * @param array $tids
  260. * The array of term IDs.
  261. *
  262. * @return array
  263. * An array of disallowed term IDs.
  264. */
  265. function taxonomy_access_create_allowed(array $tids) {
  266. $all_allowed = taxonomy_access_user_create_terms();
  267. // If the user's create grant info is exactly TRUE, all terms are allowed.
  268. if ($all_allowed === TRUE) {
  269. return $tids;
  270. }
  271. return array_intersect($tids, $all_allowed);
  272. }
  273. /**
  274. * Filter a list of vocab IDs to those in which the user may create by default.
  275. *
  276. * @param array $vids
  277. * The array of vocabulary IDs.
  278. *
  279. * @return array
  280. * An array of disallowed vocabulary IDs.
  281. */
  282. function taxonomy_access_create_default_allowed(array $vids) {
  283. $all_allowed = taxonomy_access_user_create_defaults();
  284. // If the user's create grant info is exactly TRUE, all terms are allowed.
  285. if ($all_allowed === TRUE) {
  286. return $vids;
  287. }
  288. return array_intersect($vids, $all_allowed);
  289. }
  290. /**
  291. * Retrieve vocabularies in which the current user may create terms.
  292. *
  293. * @param object|null $account
  294. * (optional) The account for which to retrieve grants. If no account is
  295. * passed, the current user will be used. Defaults to NULL.
  296. *
  297. * @return array
  298. * An array of term IDs, or TRUE if the user has the grant for all terms.
  299. */
  300. function _taxonomy_access_create_defaults($account = NULL) {
  301. // If the user can administer taxonomy, return TRUE for a global grant.
  302. if (user_access('administer taxonomy', $account)) {
  303. return TRUE;
  304. }
  305. // Build a term grant query.
  306. $query = _taxonomy_access_grant_query(array('create'), TRUE);
  307. // Select term grants for the current user's roles.
  308. if (is_null($account)) {
  309. global $user;
  310. $account = $user;
  311. }
  312. $query
  313. ->fields('td', array('vid'))
  314. ->groupBy('td.vid')
  315. ->condition('tadg.rid', array_keys($account->roles), 'IN')
  316. ;
  317. // Fetch term IDs.
  318. $r = $query->execute()->fetchAll();
  319. $vids = array();
  320. // If there are results, initialize a flag to test whether the user
  321. // has the grant for all terms.
  322. $grants_for_all_vocabs = empty($r) ? FALSE : TRUE;
  323. foreach ($r as $record) {
  324. // If the user has the grant, add the term to the array.
  325. if ($record->grant_create) {
  326. $vids[] = $record->vid;
  327. }
  328. // Otherwise, flag that the user does not have the grant for all terms.
  329. else {
  330. $grants_for_all_vocabs = FALSE;
  331. }
  332. }
  333. // If the user has the grant for all terms, return TRUE for a global grant.
  334. if ($grants_for_all_vocabs) {
  335. return TRUE;
  336. }
  337. return $vids;
  338. }
  339. /**
  340. * Autocomplete menu callback: filter allowed terms by create, not list.
  341. *
  342. * For now we essentially duplicate the code from taxonomy.module, because
  343. * it calls drupal_json_output without providing the logic separately.
  344. *
  345. * @see http://drupal.org/node/1169964
  346. * @see taxonomy_autocomplete()
  347. */
  348. function taxonomy_access_autocomplete($field_name, $tags_typed = '') {
  349. // Enforce that list grants do not filter the autocomplete.
  350. taxonomy_access_disable_list();
  351. $field = field_info_field($field_name);
  352. // The user enters a comma-separated list of tags. We only autocomplete the last tag.
  353. $tags_typed = drupal_explode_tags($tags_typed);
  354. $tag_last = drupal_strtolower(array_pop($tags_typed));
  355. $matches = array();
  356. if ($tag_last != '') {
  357. // Part of the criteria for the query come from the field's own settings.
  358. $vids = array();
  359. $vocabularies = taxonomy_vocabulary_get_names();
  360. foreach ($field['settings']['allowed_values'] as $tree) {
  361. $vids[] = $vocabularies[$tree['vocabulary']]->vid;
  362. }
  363. $query = db_select('taxonomy_term_data', 't');
  364. $query->addTag('translatable');
  365. $query->addTag('term_access');
  366. // Do not select already entered terms.
  367. if (!empty($tags_typed)) {
  368. $query->condition('t.name', $tags_typed, 'NOT IN');
  369. }
  370. // Select rows that match by term name.
  371. $tags_return = $query
  372. ->fields('t', array('tid', 'name'))
  373. ->condition('t.vid', $vids)
  374. ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE')
  375. ->range(0, 10)
  376. ->execute()
  377. ->fetchAllKeyed();
  378. // Unset suggestions disallowed by create grants.
  379. $disallowed = taxonomy_access_create_disallowed(array_keys($tags_return));
  380. foreach ($disallowed as $tid) {
  381. unset($tags_return[$tid]);
  382. }
  383. $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
  384. $term_matches = array();
  385. foreach ($tags_return as $tid => $name) {
  386. $n = $name;
  387. // Term names containing commas or quotes must be wrapped in quotes.
  388. if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
  389. $n = '"' . str_replace('"', '""', $name) . '"';
  390. }
  391. $term_matches[$prefix . $n] = check_plain($name);
  392. }
  393. }
  394. drupal_json_output($term_matches);
  395. }
  396. /**
  397. * Validates taxonomy autocomplete values for create grants.
  398. *
  399. * For now we essentially duplicate the code from taxonomy.module, because
  400. * it calls form_set_value() without providing the logic separately.
  401. *
  402. * We use two properties set in hook_field_widget_form_alter():
  403. * - $element['#allow_autocreate']
  404. * - $element['#disallowed_defaults']
  405. *
  406. * @todo
  407. * Specify autocreate per vocabulary?
  408. *
  409. * @see taxonomy_autocomplete_validate()
  410. * @see taxonomy_access_autocomplete()
  411. * @see taxonomy_access_field_widget_taxonomy_autocomplete_form_alter()
  412. */
  413. function _taxonomy_access_autocomplete_validate($element, &$form_state) {
  414. // Autocomplete widgets do not send their tids in the form, so we must detect
  415. // them here and process them independently.
  416. $value = array();
  417. if ($tags = $element['#value']) {
  418. // Collect candidate vocabularies.
  419. $field = field_widget_field($element, $form_state);
  420. $vocabularies = array();
  421. foreach ($field['settings']['allowed_values'] as $tree) {
  422. if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
  423. $vocabularies[$vocabulary->vid] = $vocabulary;
  424. }
  425. }
  426. // Translate term names into actual terms.
  427. $typed_terms = drupal_explode_tags($tags);
  428. foreach ($typed_terms as $typed_term) {
  429. // See if the term exists in the chosen vocabulary and return the tid;
  430. // otherwise, create a new 'autocreate' term for insert/update.
  431. if ($possibilities = taxonomy_term_load_multiple(array(), array('name' => trim($typed_term), 'vid' => array_keys($vocabularies)))) {
  432. $term = array_pop($possibilities);
  433. }
  434. // Only autocreate if the user has create for the vocab. default.
  435. elseif ($element['#allow_autocreate']) {
  436. $vocabulary = reset($vocabularies);
  437. $term = array(
  438. 'tid' => 'autocreate',
  439. 'vid' => $vocabulary->vid,
  440. 'name' => $typed_term,
  441. 'vocabulary_machine_name' => $vocabulary->machine_name,
  442. );
  443. }
  444. // If they cannot autocreate and this is a new term, set an error.
  445. else {
  446. form_error(
  447. $element,
  448. t('You may not create new tags in %name.',
  449. array('%name' => t($element['#title']))
  450. )
  451. );
  452. }
  453. if ($term) {
  454. $value[] = (array) $term;
  455. }
  456. }
  457. }
  458. // Add in the terms that were disallowed.
  459. // taxonomy.module expects arrays, not objects.
  460. $disallowed = taxonomy_term_load_multiple($element['#disallowed_defaults']);
  461. foreach ($disallowed as $key => $term) {
  462. $disallowed[$key] = (array) $term;
  463. }
  464. $value = array_merge($value, $disallowed);
  465. // Subsequent validation will be handled by hook_field_attach_validate().
  466. // Set the value in the form.
  467. form_set_value($element, $value, $form_state);
  468. }
  469. /**
  470. * Form element validation handler for taxonomy option fields.
  471. *
  472. * We use a property set in hook_field_widget_form_alter():
  473. * - $element['#disallowed_defaults']
  474. *
  475. * @see options_field_widget_validate()
  476. * @see taxonomy_access_field_widget_form_alter()
  477. */
  478. function _taxonomy_access_options_validate($element, &$form_state) {
  479. if ($element['#required'] && $element['#value'] == '_none') {
  480. form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
  481. }
  482. // Clone the element and add in disallowed defaults.
  483. $el = $element;
  484. if (!empty($element['#disallowed_defaults'])) {
  485. if (empty($el['#value'])) {
  486. $el['#value'] = $element['#disallowed_defaults'];
  487. }
  488. elseif (is_array($el['#value'])) {
  489. $el['#value'] = array_unique(array_merge($el['#value'], $element['#disallowed_defaults']));
  490. }
  491. else {
  492. $el['#value'] = array_unique(array_merge(array($el['#value']), $element['#disallowed_defaults']));
  493. }
  494. }
  495. // Transpose selections from field => delta to delta => field, turning
  496. // multiple selected options into multiple parent elements.
  497. $items = _options_form_to_storage($el);
  498. // Subsequent validation will be handled by hook_field_attach_validate().
  499. // Set the value in the form.
  500. form_set_value($element, $items, $form_state);
  501. }
  502. /**
  503. * Default value re-generation for autocomplete fields.
  504. *
  505. * @param array $items
  506. * An array of values from form build info, filtered by create grants.
  507. *
  508. * @return string
  509. * Field default value.
  510. *
  511. * @see taxonomy_field_widget_form()
  512. */
  513. function taxonomy_access_autocomplete_default_value(array $items) {
  514. // Preserve the original state of the list flag.
  515. $flag_state = taxonomy_access_list_enabled();
  516. // Enforce that list grants do not filter the options list.
  517. taxonomy_access_disable_list();
  518. // Assemble list of tags.
  519. $tags = array();
  520. foreach ($items as $item) {
  521. $tags[$item['tid']] = isset($item['taxonomy_term']) ? $item['taxonomy_term'] : taxonomy_term_load($item['tid']);
  522. }
  523. // Assemble the default value using taxonomy.module.
  524. $tags = taxonomy_implode_tags($tags);
  525. // Restore list flag to previous state.
  526. if ($flag_state) {
  527. taxonomy_access_enable_list();
  528. }
  529. return $tags;
  530. }
  531. /**
  532. * Validates form submissions of taxonomy fields for create grants.
  533. *
  534. * @todo
  535. * Use field label and term names in errors rather than field name and tids.
  536. *
  537. * @see http://drupal.org/node/1220212
  538. * @see entity_form_field_validate()
  539. */
  540. function _taxonomy_access_field_validate($entity_type, $entity, &$errors) {
  541. // Check for a pre-existing entity (i.e., the entity is being updated).
  542. $old_fields = FALSE;
  543. // The entity is actually a "pseudo-entity," and the user profile form
  544. // neglects to include the uid. So, we need to load it manually.
  545. if ($entity_type == 'user') {
  546. // Some modules which extend the user profile form cause additional
  547. // validation to happen with "pseudo-entities" that do not include the
  548. // name. So, check if it exists.
  549. if (isset($entity->name)) {
  550. if ($account = user_load_by_name($entity->name)) {
  551. $entity->uid = $account->uid;
  552. }
  553. }
  554. }
  555. list($entity_id, , $bundle) = entity_extract_ids($entity_type, $entity);
  556. if ($entity_id) {
  557. // Load the entity.
  558. $old_entity = entity_load($entity_type, array($entity_id));
  559. $old_entity = $old_entity[$entity_id];
  560. // Fetch the original entity's taxonomy fields.
  561. $old_fields =
  562. _taxonomy_access_entity_fields($entity_type, $old_entity, $bundle);
  563. }
  564. // Fetch the updated entity's taxonomy fields.
  565. $new_fields =
  566. _taxonomy_access_entity_fields($entity_type, $entity, $bundle);
  567. // Set errors if there are any disallowed changes.
  568. $changes = _taxonomy_access_compare_fields($new_fields, $old_fields);
  569. // We care about the overall value list, so delta is not important.
  570. $delta = 0;
  571. // Check each field and langcode for disallowed changes.
  572. foreach ($changes as $field_name => $langcodes) {
  573. foreach ($langcodes as $langcode => $disallowed) {
  574. if ($disallowed) {
  575. if (!empty($disallowed['added'])) {
  576. $text = 'You may not add the following tags to %field: %tids';
  577. $errors[$field_name][$langcode][$delta][] = array(
  578. 'error' => 'taxonomy_access_disallowed_added',
  579. 'message' => t($text, array(
  580. '%field' => $field_name,
  581. '%tids' => implode(', ', $disallowed['added']),
  582. )),
  583. );
  584. }
  585. if (!empty($disallowed['removed'])) {
  586. $text = 'You may not remove the following tags from %field: %tids';
  587. $errors[$field_name][$langcode][$delta][] = array(
  588. 'error' => 'taxonomy_access_disallowed_removed',
  589. 'message' => t($text, array(
  590. '%field' => $field_name,
  591. '%tids' => implode(', ', $disallowed['removed']),
  592. )),
  593. );
  594. }
  595. }
  596. }
  597. }
  598. }
  599. /**
  600. * Helper function to extract the taxonomy fields from an entity.
  601. *
  602. * @param object $entity
  603. * The entity object.
  604. *
  605. * @return array
  606. * An associative array of field information, containing:
  607. * - field_list: A flat array of all this entity's taxonomy fields, with the
  608. * format $field_name => $field_name.
  609. * - langcodes: A flat array of all langcodes in this entity's fields, with
  610. * the format $langcode => $langcode.
  611. * - data: An associative array of non-empty fields:
  612. * - $field_name: An associative array keyed by langcode.
  613. * - $langcode: Array of field values for this field name and langcode.
  614. *
  615. * @see http://drupal.org/node/1220168
  616. */
  617. function _taxonomy_access_entity_fields($entity_type, $entity, $bundle) {
  618. // Maintain separate lists of field names and langcodes for quick comparison.
  619. $fields = array();
  620. $fields['field_list'] = array();
  621. $fields['langcodes'] = array();
  622. $fields['data'] = array();
  623. // If there is no entity, return the empty structure.
  624. if (!$entity) {
  625. return $fields;
  626. }
  627. // Get a list of possible fields for the bundle.
  628. // The bundle does not contain the field type (see #122016), so our only use
  629. // for it is the field names.
  630. $possible = array_keys(field_info_instances($entity_type, $bundle));
  631. // Sort through the entity for relevant field data.
  632. foreach ($entity as $field_name => $field) {
  633. // Only proceed if this element is a valid field for the bundle.
  634. if (in_array($field_name, $possible, TRUE)) {
  635. // Check whether each entity field is a taxonomy field.
  636. $info = field_info_field($field_name);
  637. if ($info['type'] == 'taxonomy_term_reference') {
  638. foreach ($field as $langcode => $values) {
  639. // Add non-empty fields to the lists.
  640. if (!empty($values)) {
  641. $fields['langcodes'][$langcode] = $langcode;
  642. $fields['field_list'][$field_name] = $field_name;
  643. $fields['data'][$field_name][$langcode] = $values;
  644. }
  645. unset($values);
  646. }
  647. }
  648. }
  649. unset($info);
  650. unset($field);
  651. }
  652. unset($entity);
  653. return $fields;
  654. }
  655. /**
  656. * Helper function to compare field values and look for disallowed changes.
  657. *
  658. * @param array $new
  659. * An associative array of the updated field information as returned by
  660. * _taxonomy_access_entity_fields().
  661. * @param array $old
  662. * (optional) An associative array of the original field information,
  663. * or FALSE if there is no original field data. Defaults to FALSE.
  664. *
  665. * @return array
  666. * An array of disallowed changes, with the structure:
  667. * - $field_name: An associative array keyed by langcode.
  668. * - $langcode: Disallowed changes for this field name and langcode,
  669. * or FALSE if none.
  670. * - 'added' => An array of added terms that are disallowed.
  671. * - 'removed' => An array of removed termss that are disallowed.
  672. *
  673. * @see _taxonomy_access_entity_fields()
  674. * @see _taxonomy_access_disallowed_changes()
  675. */
  676. function _taxonomy_access_compare_fields($new, $old = FALSE) {
  677. $disallowed_changes = array();
  678. // If there are no original fields, simply process new.
  679. if (!$old) {
  680. foreach ($new['data'] as $field_name => $langcodes) {
  681. foreach ($langcodes as $langcode => $values) {
  682. $changes = _taxonomy_access_disallowed_changes($values, array());
  683. if ($changes) {
  684. if (!isset($disallowed_changes[$field_name])) {
  685. $disallowed_changes[$field_name] = array();
  686. }
  687. $disallowed_changes[$field_name][$langcode] = $changes;
  688. }
  689. }
  690. }
  691. }
  692. // Otherwise, aggregate and compare field data.
  693. else {
  694. $all_fields = $new['field_list'] + $old['field_list'];
  695. $all_langcodes = $new['langcodes'] + $old['langcodes'];
  696. foreach ($all_fields as $field_name) {
  697. foreach ($all_langcodes as $langcode) {
  698. $new_values = array();
  699. if (isset($new['field_list'][$field_name])
  700. && isset($new['data'][$field_name][$langcode])) {
  701. $new_values = $new['data'][$field_name][$langcode];
  702. }
  703. $old_values = array();
  704. if (isset($old['field_list'][$field_name])
  705. && isset($old['data'][$field_name][$langcode])) {
  706. $old_values = $old['data'][$field_name][$langcode];
  707. }
  708. $changes = _taxonomy_access_disallowed_changes($new_values, $old_values);
  709. if ($changes) {
  710. if (!isset($disallowed_changes[$field_name])) {
  711. $disallowed_changes[$field_name] = array();
  712. }
  713. $disallowed_changes[$field_name][$langcode] = $changes;
  714. }
  715. }
  716. }
  717. }
  718. unset($old);
  719. unset($new);
  720. return $disallowed_changes;
  721. }
  722. /**
  723. * Helper function to check for term reference changes disallowed by create.
  724. *
  725. * @param array $new_field
  726. * The entity or form values of the updated field.
  727. * @param array $old_field
  728. * The entity or form values of the original field.
  729. *
  730. * @return array|false
  731. * Returns FALSE if there are no disallowed changes. Otherwise, an array:
  732. * - 'added' => An array of added terms that are disallowed.
  733. * - 'removed' => An array of removed termss that are disallowed.
  734. */
  735. function _taxonomy_access_disallowed_changes(array $new_field, array $old_field) {
  736. // Assemble a list of term IDs from the original entity, if any.
  737. $old_tids = array();
  738. foreach ($old_field as $old_item) {
  739. // Some things are NULL for some reason.
  740. if ($old_item['tid']) {
  741. $old_tids[] = $old_item['tid'];
  742. }
  743. }
  744. // Assemble a list of term IDs from the updated entity.
  745. $new_tids = array();
  746. foreach ($new_field as $delta => $new_item) {
  747. // Some things are NULL for some reason.
  748. // Allow the special tid "autocreate" so users can create new terms.
  749. if ($new_item['tid'] && ($new_item['tid'] != 'autocreate')) {
  750. $new_tids[$delta] = $new_item['tid'];
  751. }
  752. }
  753. // Check for added tids, and unset ones the user may not add.
  754. $added = array_diff($new_tids, $old_tids);
  755. $may_not_add = taxonomy_access_create_disallowed($added);
  756. // Check for removed tids, and restore ones the user may not remove.
  757. $removed = array_diff($old_tids, $new_tids);
  758. $may_not_remove = taxonomy_access_create_disallowed($removed);
  759. // If there were any disallowed changes, return them.
  760. if (!empty($may_not_add) || !empty($may_not_remove)) {
  761. return array('added' => $may_not_add, 'removed' => $may_not_remove);
  762. }
  763. // Return FALSE if all changes were valid.
  764. return FALSE;
  765. }
  766. /**
  767. * End of "defgroup tac_create".
  768. * @}
  769. */