unique_field.module 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. <?php
  2. /**
  3. * @file
  4. * Provides content validation requirement that a node's author,
  5. * language, taxonomy terms, or other fields are unique.
  6. *
  7. * Unique Field module
  8. * Compatible with Drupal 7.x
  9. *
  10. * By Joe Turgeon [http://arithmetric.com]
  11. */
  12. // query scope definitions
  13. define('UNIQUE_FIELD_SCOPE_NODE', 'node');
  14. define('UNIQUE_FIELD_SCOPE_TYPE', 'type');
  15. define('UNIQUE_FIELD_SCOPE_LANGUAGE', 'language');
  16. define('UNIQUE_FIELD_SCOPE_ALL', 'all');
  17. // query comparison definitions
  18. define('UNIQUE_FIELD_COMP_EACH', 'each');
  19. define('UNIQUE_FIELD_COMP_ALL', 'all');
  20. // query field definitions
  21. define('UNIQUE_FIELD_FIELDS_TITLE', 'title');
  22. define('UNIQUE_FIELD_FIELDS_AUTHOR', 'name');
  23. define('UNIQUE_FIELD_FIELDS_LANGUAGE', 'language');
  24. // setting definition
  25. define('UNIQUE_FIELD_SHOW_MATCHES', 'show_matches');
  26. /**
  27. * Implements hook_help().
  28. */
  29. function unique_field_help($path, $arg) {
  30. switch ($path) {
  31. case 'admin/help#unique_field':
  32. $output = '<p>' . t("The Unique Field module allows administrators to require that content supplied for specified fields is unique. For example, you may require that each node has a unique title or a different author. For configuration options, please see the <em>Unique Field restrictions</em> section of a content type's administration page.") . '</p>';
  33. return $output;
  34. }
  35. }
  36. /**
  37. * Implements hook_permission().
  38. */
  39. function unique_field_permission() {
  40. return array(
  41. 'unique_field_perm_admin' => array(
  42. 'title' => t('Designate fields as unique'),
  43. 'description' => t('Configure whether any fields of a content type should be unique.'),
  44. ),
  45. 'unique_field_perm_bypass' => array(
  46. 'title' => t('Bypass requirement that fields are unique'),
  47. 'description' => t('Allows users to ignore errors about duplicate content and submit it anyway.'),
  48. ),
  49. );
  50. }
  51. /**
  52. * Implements hook_form_alter().
  53. */
  54. function unique_field_form_alter(&$form, &$form_state, $form_id) {
  55. if ($form_id === 'node_type_form' && user_access('unique_field_perm_admin') && isset($form['#node_type'])) {
  56. unique_field_node_settings_form($form);
  57. }
  58. elseif (strpos($form_id, 'node_form') !== FALSE && user_access('unique_field_perm_bypass')) {
  59. $form['unique_field_override'] = array(
  60. '#type' => 'hidden',
  61. '#default_value' => '0',
  62. );
  63. }
  64. }
  65. /**
  66. * Implements hook_node_validate().
  67. */
  68. function unique_field_node_validate($node, $form) {
  69. // skip validation if deleting a node or if override value is set
  70. if ((!empty($node->op) && !empty($node->delete) && $node->op === $node->delete) || (is_array($form) && isset($form['unique_field_override']) && is_array($form['unique_field_override']) && $form['unique_field_override']['#value'] && user_access('unique_field_perm_bypass'))) {
  71. return;
  72. }
  73. // get list of unique fields for node type
  74. $fields = variable_get('unique_field_fields_' . $node->type, array());
  75. // check if there are unique fields for this node type
  76. if (count($fields)) {
  77. // get unique field settings for this node type
  78. $scope = variable_get('unique_field_scope_' . $node->type, UNIQUE_FIELD_SCOPE_TYPE);
  79. $comp = variable_get('unique_field_comp_' . $node->type, UNIQUE_FIELD_COMP_EACH);
  80. $var_show_matches = variable_get('unique_field_show_matches_' . $node->type, array());
  81. $show_matches = is_array($var_show_matches) && in_array(UNIQUE_FIELD_SHOW_MATCHES, $var_show_matches);
  82. // initialization
  83. $errmsg = NULL;
  84. $errfld = array();
  85. $matches = array();
  86. $allmatch = NULL;
  87. // check fields for node scope
  88. if ($scope === UNIQUE_FIELD_SCOPE_NODE) {
  89. $values = array();
  90. foreach ($fields as $field) {
  91. $new_values = array();
  92. if ($field === UNIQUE_FIELD_FIELDS_TITLE) {
  93. $new_values[] = $node->title;
  94. }
  95. else {
  96. // Ensure that the field content exists. For example, if field
  97. // permissions are enabled, the field may be missing for some users.
  98. // Make sure to use the field API function, e.g. to respect language.
  99. $fvalues = field_get_items('node', $node, $field);
  100. if (empty($fvalues)) {
  101. continue;
  102. }
  103. $field_info = field_info_field($field);
  104. $field_keys = array_keys($field_info['columns']);
  105. foreach ($fvalues as $index => $value) {
  106. if (is_numeric($index)) {
  107. $field_combined = array();
  108. foreach ($field_keys as $key) {
  109. if (isset($fvalues[$index][$key]) && !empty($fvalues[$index][$key])) {
  110. $field_combined[$key] = $fvalues[$index][$key];
  111. }
  112. }
  113. if (!empty($field_combined)) {
  114. $new_values[] = serialize($field_combined);
  115. }
  116. }
  117. }
  118. }
  119. $new_values = array_merge($values, $new_values);
  120. $values = array_unique($new_values);
  121. if (serialize($values) !== serialize($new_values)) {
  122. $errfld[] = $field;
  123. }
  124. }
  125. if (count($errfld) > 0) {
  126. $errmsg = t('The @labels fields must have unique values. The @label field has a value that is already used.');
  127. }
  128. }
  129. // check fields for other scopes
  130. else {
  131. foreach ($fields as $field) {
  132. $values = '';
  133. if ($field === UNIQUE_FIELD_FIELDS_TITLE) {
  134. $values = $node->title;
  135. }
  136. elseif ($field === UNIQUE_FIELD_FIELDS_AUTHOR) {
  137. $values = $node->uid;
  138. }
  139. elseif ($field === UNIQUE_FIELD_FIELDS_LANGUAGE) {
  140. $values = $node->language;
  141. }
  142. else {
  143. // Ensure that the field content exists. For example, if field
  144. // permissions are enabled, the field may be missing for some users.
  145. // Make sure to use the field API function, e.g. to respect language.
  146. $fvalues = field_get_items('node', $node, $field);
  147. if (empty($fvalues)) {
  148. continue;
  149. }
  150. foreach ($fvalues as $index => $value) {
  151. if (!is_numeric($index) || !is_array($value)) {
  152. unset($fvalues[$index]);
  153. }
  154. }
  155. $field_info = field_info_field($field);
  156. $values = _field_filter_items($field_info, $fvalues);
  157. }
  158. if (empty($values)) {
  159. continue;
  160. }
  161. $match = unique_field_match_value($field, $values, $scope, $node->type, $node->language);
  162. // remove matches of this node
  163. if ($node->nid && is_array($match) && in_array($node->nid, $match)) {
  164. $key = array_search($node->nid, $match);
  165. unset($match[$key]);
  166. }
  167. if ($comp === UNIQUE_FIELD_COMP_EACH && is_array($match) && count($match)) {
  168. $errfld[] = $field;
  169. $errmsg = t('The @label field requires a unique value, and the specified value is already used.');
  170. }
  171. $matches[$field] = $match;
  172. $allmatch = is_array($allmatch) ? array_intersect($allmatch, $match) : $match;
  173. }
  174. // check for fields in combination
  175. if ($comp === UNIQUE_FIELD_COMP_ALL && is_array($allmatch) && count($allmatch)) {
  176. foreach ($fields as $field) {
  177. $errfld[] = $field;
  178. $matches[$field] = $allmatch;
  179. }
  180. $errmsg = t('This form requires that the fields @labels are a unique combination. The specified values are already used.');
  181. }
  182. }
  183. // common error messages
  184. if ($errmsg && !empty($errmsg) && is_array($errfld) && count($errfld) > 0) {
  185. $labels = array();
  186. foreach ($errfld as $field) {
  187. if ($field === UNIQUE_FIELD_FIELDS_TITLE) {
  188. $nodetype = node_type_get_type($node->type);
  189. $labels[$field] = $nodetype->title_label;
  190. }
  191. elseif ($field === UNIQUE_FIELD_FIELDS_AUTHOR) {
  192. $labels[$field] = t('Author');
  193. }
  194. elseif ($field === UNIQUE_FIELD_FIELDS_LANGUAGE) {
  195. $labels[$field] = t('Language');
  196. }
  197. else {
  198. $fld = field_info_instance('node', $field, $node->type);
  199. $labels[$field] = $fld['label'];
  200. }
  201. }
  202. foreach ($errfld as $field) {
  203. $msg = strtr($errmsg, array('@label' => check_plain($labels[$field]), '@labels' => check_plain(join(', ', $labels))));
  204. if ($show_matches && isset($matches[$field]) && is_array($matches[$field]) && count($matches[$field])) {
  205. $list_items = array();
  206. foreach ($matches[$field] as $nid) {
  207. $match_node = node_load($nid);
  208. if (node_access('view', $match_node)) {
  209. $list_items[] = l($match_node->title, 'node/' . $nid);
  210. }
  211. }
  212. $list_html = theme('item_list', array('items' => $list_items));
  213. $msg .= ' ' . t('Matches are found in the following content: !list-html', array('!list-html' => $list_html));
  214. }
  215. if (user_access('unique_field_perm_bypass')) {
  216. $form_id = str_replace('_', '-', $form['#id']);
  217. $msg .= '<p>' . t('Click !here to bypass this check and resubmit.', array('!here' => "<a href=\"#\" onclick=\"jQuery('form#" . $form_id . " input[name=\'unique_field_override\']').val(1);jQuery('form#" . $form_id . "').submit();return false;\">" . t('here') . '</a>')) . '</p>';
  218. }
  219. form_set_error($field, $msg);
  220. // if checking the fields in combination, then one error message
  221. // is enough for all of the fields
  222. if ($comp === UNIQUE_FIELD_COMP_ALL) {
  223. break;
  224. }
  225. }
  226. }
  227. }
  228. }
  229. /**
  230. * Find nodes with a matching field value within a given scope.
  231. */
  232. function unique_field_match_value($field, $values, $scope, $ntype = NULL, $nlanguage = NULL) {
  233. // Initialize EntityFieldQuery
  234. $entity_type = 'node';
  235. $efq = new EntityFieldQuery();
  236. $efq->entityCondition('entity_type', $entity_type);
  237. // Add query conditions depending on the type of field
  238. if ($field === UNIQUE_FIELD_FIELDS_TITLE) {
  239. // Query condition for bundle title property
  240. $efq->propertyCondition('title', $values);
  241. }
  242. elseif ($field === UNIQUE_FIELD_FIELDS_AUTHOR) {
  243. // Query condition for bundle author property
  244. $efq->propertyCondition('uid', $values);
  245. }
  246. elseif ($field === UNIQUE_FIELD_FIELDS_LANGUAGE) {
  247. // Query condition for bundle language property
  248. $efq->propertyCondition('language', $values);
  249. }
  250. else {
  251. // Query condition for other fields
  252. $field_info = field_info_field($field);
  253. // Check all items/values
  254. foreach ($values as $index => $value) {
  255. // Check all columns
  256. foreach ($value as $key => $val) {
  257. // Skip values that are not stored in the database
  258. if (!isset($field_info['columns'][$key]) || !is_array($field_info['columns'][$key])) {
  259. continue;
  260. }
  261. // Skip if the value is an empty string or is not a scalar
  262. if (!strlen($val) || !is_scalar($val)) {
  263. continue;
  264. }
  265. $efq->fieldCondition($field, $key, $val);
  266. }
  267. }
  268. }
  269. // Add query conditions for the scope setting
  270. if ($scope === UNIQUE_FIELD_SCOPE_TYPE && !empty($ntype)) {
  271. // Query condition for the bundle (content type) scope
  272. $efq->entityCondition('bundle', $ntype);
  273. }
  274. elseif ($scope === UNIQUE_FIELD_SCOPE_LANGUAGE && !empty($nlanguage)) {
  275. // Query condition for the language scope
  276. $efq->propertyCondition('language', $nlanguage);
  277. }
  278. // Execute query and collect results
  279. $result = $efq->execute();
  280. $nids = array();
  281. if (isset($result[$entity_type]) && is_array($result[$entity_type])) {
  282. foreach ($result[$entity_type] as $item) {
  283. if (!empty($item->nid)) {
  284. $nids[] = $item->nid;
  285. }
  286. }
  287. }
  288. return array_unique($nids);
  289. }
  290. /**
  291. * Add the unique field settings form to content type forms (node_type_form).
  292. */
  293. function unique_field_node_settings_form(&$form) {
  294. // load fields for content type
  295. $ntype = $form['#node_type']->type;
  296. $nodetype = !empty($ntype) ? node_type_get_type($ntype) : FALSE;
  297. $fieldopts = array();
  298. $fieldopts[UNIQUE_FIELD_FIELDS_TITLE] = ($nodetype && !empty($nodetype->title_label)) ? check_plain($nodetype->title_label) . ' (' . t('title') . ')' : t('Title');
  299. $fieldopts[UNIQUE_FIELD_FIELDS_AUTHOR] = t('Author');
  300. if (module_exists('locale') && !empty($ntype) && variable_get('language_content_type_' . $ntype, 0)) {
  301. $fieldopts[UNIQUE_FIELD_FIELDS_LANGUAGE] = t('Language');
  302. }
  303. if (!empty($ntype)) {
  304. $fields = field_info_instances('node', $ntype);
  305. foreach ($fields as $fieldname => $info) {
  306. $fieldopts[$fieldname] = $info['label'] . ' (' . $fieldname . ')';
  307. }
  308. }
  309. // build the form
  310. $form['unique_field'] = array(
  311. '#type' => 'fieldset',
  312. '#title' => t('Unique Field restrictions'),
  313. '#weight' => 1,
  314. '#collapsible' => TRUE,
  315. '#collapsed' => TRUE,
  316. '#group' => 'additional_settings',
  317. );
  318. $form['unique_field']['unique_field_fields'] = array(
  319. '#type' => 'checkboxes',
  320. '#title' => t('Choose the fields that should be unique'),
  321. '#options' => $fieldopts,
  322. '#default_value' => !empty($ntype) ? variable_get('unique_field_fields_' . $ntype, array()) : array(),
  323. '#description' => t('After designating that certain fields should be unique, users will not be able to submit the content form to create a new node or update an existing one if it contains values in the designated fields that duplicate others.'),
  324. );
  325. $form['unique_field']['unique_field_scope'] = array(
  326. '#type' => 'radios',
  327. '#title' => t('Choose the scope for the unique values'),
  328. '#options' => array(
  329. UNIQUE_FIELD_SCOPE_TYPE => t('Content type'),
  330. UNIQUE_FIELD_SCOPE_LANGUAGE => t('Language'),
  331. UNIQUE_FIELD_SCOPE_ALL => t('All nodes'),
  332. UNIQUE_FIELD_SCOPE_NODE => t('Single node only'),
  333. ),
  334. '#default_value' => !empty($ntype) ? variable_get('unique_field_scope_' . $ntype, UNIQUE_FIELD_SCOPE_TYPE) : UNIQUE_FIELD_SCOPE_TYPE,
  335. '#description' => t('Choose whether the values in the specified fields must be unique among nodes of this content type, among nodes of the same language, among all nodes, or only among the fields of the present node.'),
  336. );
  337. $form['unique_field']['unique_field_comp'] = array(
  338. '#type' => 'radios',
  339. '#title' => t('Choose whether values must be unique individually or in combination'),
  340. '#options' => array(
  341. UNIQUE_FIELD_COMP_EACH => t('Each of the specified fields must have a unique value'),
  342. UNIQUE_FIELD_COMP_ALL => t('The combination of values from the specified fields must be unique'),
  343. ),
  344. '#default_value' => !empty($ntype) ? variable_get('unique_field_comp_' . $ntype, UNIQUE_FIELD_COMP_EACH) : UNIQUE_FIELD_COMP_EACH,
  345. '#description' => t('For example, if you have fields for the parts of a street address (street number and name, city, and zip code) on a node, and want to allow only one node per complete address, but not only one node per city or per zip code, then you would want to choose that the fields must be unique in combination.'),
  346. );
  347. $form['unique_field']['unique_field_show_matches'] = array(
  348. '#type' => 'checkboxes',
  349. '#title' => t('Check this box to show which nodes match when duplicate values are found'),
  350. '#options' => array(UNIQUE_FIELD_SHOW_MATCHES => t('Enabled')),
  351. '#default_value' => !empty($ntype) ? variable_get('unique_field_show_matches_' . $ntype, array()) : array(),
  352. );
  353. // add validation function
  354. $form['#validate'][] = 'unique_field_node_settings_form_validate';
  355. }
  356. /**
  357. * Form validation callback for unique_field_node_settings_form.
  358. */
  359. function unique_field_node_settings_form_validate($form, &$form_state) {
  360. if ($form_state['values']['unique_field_scope'] === UNIQUE_FIELD_SCOPE_NODE) {
  361. if ($form_state['values']['unique_field_comp'] === UNIQUE_FIELD_COMP_ALL) {
  362. form_set_error('unique_field_comp', t('The scope of a single node requires that each field must be unique.'));
  363. }
  364. if (($form_state['values']['unique_field_fields'][UNIQUE_FIELD_FIELDS_AUTHOR] === UNIQUE_FIELD_FIELDS_AUTHOR) || (isset($form_state['values']['unique_field_fields'][UNIQUE_FIELD_FIELDS_LANGUAGE]) && $form_state['values']['unique_field_fields'][UNIQUE_FIELD_FIELDS_LANGUAGE] === UNIQUE_FIELD_FIELDS_LANGUAGE)) {
  365. form_set_error('unique_field_fields', t('The author and language fields are not supported within the scope of a single node.'));
  366. }
  367. }
  368. }