Browse Source

Showroom feature

Bachir Soussi Chiadmi 7 years ago
parent
commit
7abf64be00
22 changed files with 7962 additions and 21 deletions
  1. 76 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/CHANGELOG.txt
  2. 50 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/INSTALL.txt
  3. 339 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/LICENSE.txt
  4. 106 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/README.txt
  5. 29 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/UPDATE.txt
  6. BIN
      sites/all/modules/contrib/taxonomy/taxonomy_access/images/add.png
  7. 60 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/tac_create.js
  8. 910 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.admin.inc
  9. 873 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.create.inc
  10. 94 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.css
  11. 14 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.info
  12. 302 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.install
  13. 1775 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.module
  14. 1620 0
      sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.test
  15. 66 0
      sites/all/modules/features/showroom/showroom.features.field_base.inc
  16. 937 0
      sites/all/modules/features/showroom/showroom.features.field_instance.inc
  17. 18 0
      sites/all/modules/features/showroom/showroom.features.inc
  18. 238 2
      sites/all/modules/features/showroom/showroom.features.user_permission.inc
  19. 4 16
      sites/all/modules/features/showroom/showroom.features.user_role.inc
  20. 84 3
      sites/all/modules/features/showroom/showroom.info
  21. 31 0
      sites/all/modules/features/showroom/showroom.rules_defaults.inc
  22. 336 0
      sites/all/modules/features/showroom/showroom.strongarm.inc

+ 76 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/CHANGELOG.txt

@@ -0,0 +1,76 @@
+For complete changelog, see:
+http://drupalcode.org/project/taxonomy_access.git/log/refs/heads/7.x-1.x
+
+Taxonomy Access 7.x-1.x-dev, xxxx-xx-xx
+---------------------------------------
+
+Taxonomy Access 7.x-1.0, 2015-09-18
+---------------------------------------
+o The return value of taxonomy_access_global_defaults() has changed. Callers
+  may use _taxonomy_access_format_grant_record() to format each element of the
+  return array for hook_node_access_records().
+
+o The following constants have been added:
+  - TAXONOMY_ACCESS_GLOBAL_DEFAULT = 0
+  - TAXONOMY_ACCESS_VOCABULARY_DEFAULT = 0
+  - TAXONOMY_ACCESS_NODE_ALLOW = 1
+  - TAXONOMY_ACCESS_NODE_IGNORE = 0
+  - TAXONOMY_ACCESS_NODE_DENY = 2
+  - TAXONOMY_ACCESS_GLOBAL_DEFAULT = 0
+  - TAXONOMY_ACCESS_GLOBAL_DEFAULT = 0
+
+o Drupal core 7.8 is now explicitly required.
+
+o The "Add tag" (create) grant now defaults to "Allow" for anonymous and
+  authenticated users upon installation. (Existing installations will not be
+  affected.)
+
+Taxonomy Access 7.x-1.x-rc1, 2011-09-09
+---------------------------------------
+o Administrative paths have changed.
+
+o Renamed grant realm from 'term_access' to 'taxonomy_access_role'.
+
+o Field widgets are now automatically hidden if the user cannot add any terms.
+
+o The vocabulary default for the "Add tag" grant (create op) now controls
+  whether new terms can be created in the vocabulary in autocomplete fields.
+
+o Moved "Add tag" (create op) functionality from hook_form_alter() to
+  hook_field_widget_form_alter().
+
+o Terms disallowed by "Add tag" (create op) are disabled rather than removed.
+  This may be a configurable setting in the future.
+
+o "Add tag" (create op) now allows selection of allowed child terms when
+  the parent term is disabled.
+
+o Optimized grant update functionality to reduce queries.
+
+o Renamed several API functions:
+
+  - from: taxonomy_access_grant_update()
+    to:   taxonomy_access_set_term_grants()
+
+  - from: taxonomy_access_defaults_update()
+    to:   taxonomy_access_set_default_grants()
+
+  - form: taxonomy_access_recursive_grant_update()
+    to:   taxonomy_access_set_recursive_grants()
+
+  - from: taxonomy_access_delete_roles()
+    to:   taxonomy_access_delete_role_grants()
+
+  - from: taxonomy_access_delete_terms()
+    to:   taxonomy_access_delete_term_grants()
+
+  - from: taxonomy_access_delete_defaults()
+    to:   taxonomy_access_delete_default_grants()
+
+o Renamed "List" and "Create" grants to "View tag" and "Add tag" for clarity.
+
+o Automatically update node access as needed on shutdown.  
+  Hooks should merely add their list of nodes to 
+  taxonomy_access_affected_nodes() to be processed at the end of the request.
+
+o Provide record deletion API.

+ 50 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/INSTALL.txt

@@ -0,0 +1,50 @@
+TO INSTALL, simply install and enable the module, in these steps.
+
+PLEASE CHECK that you use the right version of Taxonomy Access for your 
+  version of DRUPAL.
+
+IMPORTANT: This is a complicated module. When you first learn to use this 
+  module, ALWAYS TRY IT FIRST ON A TEST SITE.
+
+NOTE: If you want to USE TWO OR MORE "ACCESS" MODULES AT THE SAME TIME, TEST 
+  THEM CAREFULLY. (e.g: OG, node_privacy_by_role, taxonomy access, etc.)
+
+TO UPDATE from previous versions of taxonomy_access: PLEASE READ UPDATE.TXT!
+  WHEN UPDATING, ALWAYS disable the module first before uploading the new 
+  module, on the page:
+  "Administration >> Modules"
+  (http://www.example.com/admin/modules).
+
+-----------------------
+INSTALLATION
+-----------------------
+
+1. COPY the taxonomy_access directory to your Drupal 
+   installation's module directory.
+   (By default: sites/all/modules/taxonomy_access/ in your Drupal directory.)
+
+2. ENABLE THE MODULE on page: 
+   "Administration >> Modules"
+   (http://www.example.com/admin/modules).
+
+3. REBUILD YOUR NODE ACCESS PERMISSIONS on page:
+   "Administration >> Reports >> Status report >> Node Access Permissions"
+   (http://www.example.com/admin/reports/status/rebuild).
+
+4. GRANT ADMINISTRATORS CONTROL of Taxonomy Access on page:
+   "Administration >> People >> Permissions"
+   (http://www.example.com/admin/people/permissions).
+
+   To administer Taxonomy Access, administrators must have "access 
+   administration pages" checked (under "system module") and "administer 
+   permissions" checked (under "user module"). 
+
+5. CONFIGURE ACCESS GRANTS to the various categories at: 
+   "Administration >> Configuration >> Taxonomy access control"
+   (http://www.example.com/admin/config/people/taxonomy_access).
+
+   Be sure to configure the authenticated role, as users with custom roles 
+   also have the authenticated user role.
+
+NOTE: DATABASE TABLES are automatically added to database. Module creates two 
+   database tables: 'taxonomy_access_term' and 'taxonomy_access_default'.

+ 339 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/LICENSE.txt

@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.

+ 106 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/README.txt

@@ -0,0 +1,106 @@
+-----------------------
+GENERAL DESCRIPTION
+-----------------------
+This module allows you to set access permissions for various taxonomy 
+categories based on user role.  
+
+There are permissions to VIEW, UPDATE, and DELETE nodes in each category.
+Additionally, the ADD TAG permission control whether the user can add a 
+taxonomy term to a node, and the VIEW TAG permission controls whether the user
+can see the taxonomy term listed on the node.
+
+
+-----------------------
+HELP PAGES
+-----------------------
+For more information about how to control access permissions with the Taxonomy
+access control module (TAC), see the module's help page at:
+"Administration >> Help >> Taxonomy access control"
+(admin/help/taxonomy_access).
+
+Also see the help pages at drupal.org: http://drupal.org/node/31601
+
+
+-----------------------
+DATABASE TABLES
+-----------------------
+Module creates two tables in database: 'taxonomy_access_term' and
+'taxonomy_access_default'
+
+
+-----------------------
+TROUBLESHOOTING
+-----------------------
+
+If users can view or edit pages that they do not have permission for:
+
+1. Make sure the user role does not have "administer nodes" permission.  This 
+   permission will override any settings from Taxonomy Access.
+
+2. Check whether the user role has "edit any [type] content" permissions 
+   under "node module" on the page: 
+   "Administration >> People >> Permissions"
+   (http://www.example.com/admin/people/permissions).
+
+   Granting this permission overrides TAC's "Update" permissions for the given 
+   content type, so you will not be able to deny the role edit access to any 
+   nodes in that type.  (The same is true of "delete any [type] content" 
+   permissions.)
+
+3. Check to see if the user has other roles that may be granting other 
+   permissions. Remember: Deny overrides Allow within a role, but Allow from 
+   one role can override Deny from another.
+
+4. Review the configuration for the authenticated user role on page:
+   "Administration >> People >> Permissions"
+   (http://www.example.com/admin/people/permissions).
+
+   Remember that users with custom roles also have the authenticated role, so 
+   they gain any permissions granted that role.
+
+5. Check whether you have ANY OTHER node access modules installed.
+   Other modules can override TAC's grants.
+
+6. Do a General Database Housekeeping
+  (Tables: 'node_access','taxonomy_access_term' and 'taxonomy_access_default'):
+
+   First DISABLE, then RE-ENABLE the Taxonomy access module on page:
+   "Administration >> Modules"
+   (http://www.example.com/admin/modules).
+    
+  This will force the complete rebuild of the 'node_access' table.
+  
+7. For debugging, install devel_node_access module (Devel project).
+   This can show you some information about node_access values in 
+   the database when viewing a node page.
+
+8. Force rebuilding of the permissions cache (table 'node_access'):
+   "Rebuild permissions" button on page:
+   "Administration >> Reports >> Status report >> Node Access Permissions"
+   (http://www.example.com/admin/reports/status/rebuild).
+
+   If the site is experiencing problems with permissions to content, you may
+   have to rebuild the permissions cache. Possible causes for permission
+   problems are disabling modules or configuration changes to permissions.
+   Rebuilding will remove all privileges to posts, and replace them with
+   permissions based on the current modules and settings.
+
+-----------------------
+UNINSTALLING
+-----------------------
+
+1. First DISABLE the Taxonomy access module on page:
+   "Administration >> Modules"
+   (http://www.example.com/admin/modules).
+
+2. After disabling, you can uninstall completely by choosing Taxonomy
+   Access on page: 
+   "Administration >> Modules >> Uninstall"
+   (http://www.example.com/admin/modules/uninstall).
+
+   This will remove all your settings of Taxonomy Access: variables and tables
+   ('taxonomy_access_term' and 'taxonomy_access_default').
+
+3. After uninstalling, if the site is experiencing problems with permissions to
+   content, you can rebuild the permission cache.
+   See "Troubleshooting" #8.

+ 29 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/UPDATE.txt

@@ -0,0 +1,29 @@
+READ THIS FILE if you are updating from previous versions of 
+  'taxonomy_access.module'.
+
+If you are installing taxonomy_access.module for the first time, you may ignore
+  this file.
+
+-----------------------
+UPDATING
+-----------------------
+
+1. DISABLE THE MODULE on page: 
+   "Administration >> Modules"
+   (http://www.example.com/admin/modules).
+
+2. BACK UP your database.
+
+3. COPY the new taxonomy_access directory over the existing module directory
+   (By default: sites/all/modules/taxonomy_access/ in your Drupal directory.)
+
+4. LOG IN AS MAIN ADMINISTRATOR (user with user ID 1).
+
+5. ENABLE THE MODULE on page: 
+   "Administration >> Modules"
+   (http://www.example.com/admin/modules).
+
+6. RUN UPDATE.PHP by visiting:
+   http://www.example.com/update.php
+
+7. TEST YOUR SITE'S ACCESS CONTROL.  If there are problems, see the README.txt.

BIN
sites/all/modules/contrib/taxonomy/taxonomy_access/images/add.png


+ 60 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/tac_create.js

@@ -0,0 +1,60 @@
+/**
+ * Disable disallowed terms in taxonomy fields, and re-enable on submit.
+ *
+ * We do this in jQuery because FAPI does not yet support it:
+ * @see
+ *   http://drupal.org/node/284917
+ * @see
+ *   http://drupal.org/node/342316
+ *
+ * @todo
+ *   Use clearer coding standards.
+ * @see
+ *   http://jsdemystified.drupalgardens.com/
+ */
+Drupal.behaviors.tac_create = {};
+Drupal.behaviors.tac_create.attach = function(context, settings) {
+  var $ = jQuery;
+  var $fields = $(Drupal.settings.taxonomy_access);
+
+  // For each controlled field, disable disallowed terms.
+  $.each($fields, function(i, field) {
+    var fieldname = "." + field.field;
+
+    // Disable disallowed term and its label, if any.
+    $.each(field.disallowed_tids, function(j, tid) {
+
+      // Children of the widget element with the specified tid as a value.
+      // Can be either <option> or <input>.
+      // .tac_fieldname [value='1']
+      selector = fieldname + " [value='" + tid + "']";
+      $(selector).attr('disabled','disabled');
+
+      // Label sibling adjacent the child element.
+      // .tac_fieldname [value='1'] + label
+      label_selector = fieldname + " [value='" + tid + "']" + " + label";
+      $(label_selector).attr('class','option disabled');
+    });
+  });
+
+  // Re-enable and re-select disallowed defaults on submit.
+  $("form").submit(function() {
+
+    // For each controlled field, re-enable disallowed terms.
+    $.each($fields, function(i, field) {
+      var fieldname = "." + field.field;
+
+      // Enable and select disallowed defaults.
+      $.each(field.disallowed_defaults, function(j, tid) {
+
+        // Children of the widget element with the specified tid as a value.
+        // Can be either <option> or <input>.
+        // .tac_fieldname [value='1']
+        selector = fieldname + " [value='" + tid + "']";
+        $(selector).attr('disabled','');
+        $(selector).attr('selected','selected');
+      });
+    });
+  });
+
+}

+ 910 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.admin.inc

@@ -0,0 +1,910 @@
+<?php
+
+/**
+ * @file
+ * Administrative interface for taxonomy access control.
+ */
+
+/**
+ * Page callback: Renders the TAC permissions administration overview page.
+ *
+ * @return
+ *   Form to render.
+ *
+ * @see taxonomy_access_menu()
+ */
+function taxonomy_access_admin() {
+  $roles = _taxonomy_access_user_roles();
+  $active_rids = db_query(
+    'SELECT rid FROM {taxonomy_access_default} WHERE vid = :vid',
+    array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT)
+  )->fetchCol();
+
+  $header = array(t('Role'), t('Status'), t('Operations'));
+  $rows = array();
+
+  foreach ($roles as $rid => $name) {
+    $row = array();
+    $row[] = $name;
+
+    if (in_array($rid, $active_rids)) {
+      // Add edit operation link for active roles.
+      $row[] = array('data' => t('Enabled'));
+
+    }
+    else {
+      // Add enable link for unconfigured roles.
+      $row[] = array('data' => t('Disabled'));
+    }
+    $row[] = array('data' => l(
+      t("Configure"),
+      TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit",
+      array('attributes' => array('class' => array('module-link', 'module-link-configure')))));
+    $rows[] = $row;
+  }
+
+  $build['role_table'] = array(
+    '#theme' => 'table',
+    '#header' => $header,
+    '#rows' => $rows,
+  );
+
+  return $build;
+}
+
+/**
+ * Form constructor for a form to to delete access rules for a particular role.
+ *
+ * @param int $rid
+ *   The role ID to disable.
+ *
+ * @see taxonomy_access_role_delete_confirm_submit()
+ * @see taxonomy_access_menu()
+ * @ingroup forms
+ */
+function taxonomy_access_role_delete_confirm($form, &$form_state, $rid) {
+  $roles = _taxonomy_access_user_roles();
+  if (taxonomy_access_role_enabled($rid)) {
+    $form['rid'] = array(
+      '#type' => 'value',
+      '#value' => $rid,
+    );
+    return confirm_form($form,
+      t("Are you sure you want to delete all taxonomy access rules for the role %role?",
+        array('%role' => $roles[$rid])),
+      TAXONOMY_ACCESS_CONFIG . '/role/%/edit',
+      t('This action cannot be undone.'),
+      t('Delete all'),
+      t('Cancel'));
+  }
+}
+
+/**
+ * Form submission handler for taxonomy_role_delete_confirm().
+ */
+function taxonomy_access_role_delete_confirm_submit($form, &$form_state) {
+  $roles = _taxonomy_access_user_roles();
+  $rid = $form_state['values']['rid'];
+  if (is_numeric($rid)
+      && $rid != DRUPAL_ANONYMOUS_RID
+      && $rid != DRUPAL_AUTHENTICATED_RID) {
+    if ($form_state['values']['confirm']) {
+      $form_state['redirect'] = TAXONOMY_ACCESS_CONFIG;
+      taxonomy_access_delete_role_grants($rid);
+      drupal_set_message(t('All taxonomy access rules deleted for role %role.',
+          array('%role' => $roles[$rid])));
+    }
+  }
+}
+
+/**
+ * Generates a URL to enable a role with a token for CSRF protection.
+ *
+ * @param int $rid
+ *   The role ID.
+ *
+ * @return string
+ *   The full URL for the request path.
+ */
+function taxonomy_access_enable_role_url($rid) {
+  // Create a query array with a token to validate the sumbission.
+  $query = drupal_get_destination();
+  $query['token'] = drupal_get_token($rid);
+
+  // Build and return the URL with the token and destination.
+  return url(
+    TAXONOMY_ACCESS_CONFIG . "/role/$rid/enable",
+    array('query' => $query)
+  );
+}
+
+/**
+ * Page callback: Enables a role if the proper token is provided.
+ *
+ * @param int $rid
+ *   The role ID.
+ */
+function taxonomy_access_enable_role_validate($rid) {
+  $rid = intval($rid);
+  // If a valid token is not provided, return a 403.
+  $query = drupal_get_query_parameters();
+  if (empty($query['token']) || !drupal_valid_token($query['token'], $rid)) {
+    return MENU_ACCESS_DENIED;
+  }
+  // Return a 404 for the anonymous or authenticated roles.
+  if (in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+    return MENU_NOT_FOUND;
+  }
+  // Return a 404 for invalid role IDs.
+  $roles = _taxonomy_access_user_roles();
+  if (empty($roles[$rid])) {
+    return MENU_NOT_FOUND;
+  }
+
+  // If the parameters pass validation, enable the role and complete redirect.
+  if (taxonomy_access_enable_role($rid)) {
+    drupal_set_message(t('Role %name enabled successfully.',
+      array('%name' => $roles[$rid])));
+  }
+  drupal_goto();
+}
+
+/**
+ * Form constructor for a form to manage grants by role.
+ *
+ * @param int $rid
+ *   The role ID.
+ *
+ * @see taxonomy_access_admin_form_submit()
+ * @see taxonomy_access_menu()
+ * @ingroup forms
+ */
+function taxonomy_access_admin_role($form, $form_state, $rid) {
+  // Always include the role ID in the form.
+  $rid = intval($rid);
+  $form['rid'] = array('#type' => 'value', '#value' => $rid);
+
+  // For custom roles, allow the user to enable or disable grants for the role.
+  if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+    $roles = _taxonomy_access_user_roles();
+
+    // If the role is not enabled, return only a link to enable it.
+    if (!taxonomy_access_role_enabled($rid)) {
+      $form['status'] = array(
+        '#markup' => '<p>' . t(
+          'Access control for the %name role is disabled. <a href="@url">Enable @name</a>.',
+          array(
+            '%name' => $roles[$rid],
+            '@name' => $roles[$rid],
+            '@url' => taxonomy_access_enable_role_url($rid))) . '</p>'
+      );
+      return $form;
+    }
+    // Otherwise, add a link to disable and build the rest of the form.
+    else {
+      $disable_url = url(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/delete",
+        array('query' => drupal_get_destination())
+      );
+      $form['status'] = array(
+        '#markup' => '<p>' . t(
+          'Access control for the %name role is enabled. <a href="@url">Disable @name</a>.',
+          array('@name' => $roles[$rid], '%name' => $roles[$rid], '@url' => $disable_url)) . '</p>'
+      );
+    }
+  }
+
+  // Retrieve role grants and display an administration form.
+  // Disable list filtering while preparing this form.
+  taxonomy_access_disable_list();
+
+  // Fetch all grants for the role.
+  $defaults =
+    db_query(
+      'SELECT vid, grant_view, grant_update, grant_delete, grant_create,
+              grant_list
+       FROM {taxonomy_access_default}
+       WHERE rid = :rid',
+      array(':rid' => $rid))
+    ->fetchAllAssoc('vid', PDO::FETCH_ASSOC);
+
+  $records =
+    db_query(
+      'SELECT ta.tid, td.vid, ta.grant_view, ta.grant_update, ta.grant_delete,
+              ta.grant_create, ta.grant_list
+       FROM {taxonomy_access_term} ta
+       INNER JOIN {taxonomy_term_data} td ON ta.tid = td.tid
+       WHERE rid = :rid',
+      array(':rid' => $rid))
+    ->fetchAllAssoc('tid', PDO::FETCH_ASSOC);
+  $term_grants = array();
+  foreach ($records as $record) {
+    $term_grants[$record['vid']][$record['tid']] = $record;
+  }
+
+  // Add a fieldset for the global default.
+  $form['global_default'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Global default'),
+    '#description' => t('The global default controls access to untagged nodes. It is also used as the default for disabled vocabularies.'),
+    '#collapsible' => TRUE,
+    // Collapse if there are vocabularies configured.
+    '#collapsed' => (sizeof($defaults) > 1),
+  );
+  // Print term grant table.
+  $form['global_default']['grants'] = taxonomy_access_grant_add_table($defaults[TAXONOMY_ACCESS_GLOBAL_DEFAULT], TAXONOMY_ACCESS_VOCABULARY_DEFAULT);
+
+  // Fetch all vocabularies and determine which are enabled for the role.
+  $vocabs = array();
+  $disabled = array();
+  foreach (taxonomy_get_vocabularies() as $vocab) {
+    $vocabs[$vocab->vid] = $vocab;
+    if (!isset($defaults[$vocab->vid])) {
+      $disabled[$vocab->vid] = $vocab->name;
+    }
+  }
+
+  // Add a fieldset to enable vocabularies.
+  if (!empty($disabled)) {
+    $form['enable_vocabs'] = array(
+      '#type' => 'fieldset',
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#title' => t('Add vocabulary'),
+      '#attributes' => array('class' => array('container-inline', 'taxonomy-access-add')),
+    );
+    $form['enable_vocabs']['enable_vocab'] = array(
+      '#type' => 'select',
+      '#title' => t('Vocabulary'),
+      '#options' => $disabled,
+    );
+    $form['enable_vocabs']['add'] = array(
+      '#type' => 'submit',
+      '#submit' => array('taxonomy_access_enable_vocab_submit'),
+      '#value' => t('Add'),
+    );
+  }
+
+  // Add a fieldset for each enabled vocabulary.
+  foreach ($defaults as $vid => $vocab_default) {
+    if (!empty($vocabs[$vid])) {
+      $vocab = $vocabs[$vid];
+      $name = $vocab->machine_name;
+
+      // Fetch unconfigured terms and reorder term records by hierarchy.
+      $sort = array();
+      $add_options = array();
+      if ($tree = taxonomy_get_tree($vid)) {
+        foreach ($tree as $term) {
+          if (empty($term_grants[$vid][$term->tid])) {
+            $add_options["term $term->tid"] = str_repeat('-', $term->depth) . ' ' .check_plain($term->name);
+          }
+          else {
+            $sort[$term->tid] = $term_grants[$vid][$term->tid];
+            $sort[$term->tid]['name'] =  str_repeat('-', $term->depth) . ' ' . check_plain($term->name);
+          }
+        }
+        $term_grants[$vid] = $sort;
+      }
+
+      $grants = array(TAXONOMY_ACCESS_VOCABULARY_DEFAULT => $vocab_default);
+      $grants[TAXONOMY_ACCESS_VOCABULARY_DEFAULT]['name'] = t('Default');
+      if (!empty($term_grants[$vid])) {
+        $grants += $term_grants[$vid];
+      }
+      $form[$name] = array(
+        '#type' => 'fieldset',
+        '#title' => $vocab->name,
+        '#attributes' => array('class' => array('taxonomy-access-vocab')),
+        '#description' => t('The default settings apply to all terms in %vocab that do not have their own below.', array('%vocab' => $vocab->name)),
+        '#collapsible' => TRUE,
+        '#collapsed' => FALSE,
+      );
+      // Term grant table.
+      $form[$name]['grants'] =
+        taxonomy_access_grant_table($grants, $vocab->vid, t('Term'), !empty($term_grants[$vid]));
+      // Fieldset to add a new term if there are any.
+      if (!empty($add_options)) {
+        $form[$name]['new'] = array(
+          '#type' => 'fieldset',
+          '#collapsible' => TRUE,
+          '#collapsed' => TRUE,
+          '#title' => t('Add term'),
+          '#tree' => TRUE,
+          '#attributes' => array('class' => array('container-inline', 'taxonomy-access-add')),
+        );
+        $form[$name]['new'][$vid]['item'] = array(
+          '#type' => 'select',
+          '#title' => t('Term'),
+          '#options' => $add_options,
+        );
+        $form[$name]['new'][$vid]['recursive'] = array(
+          '#type' => 'checkbox',
+          '#title' => t('with descendants'),
+        );
+        $form[$name]['new'][$vid]['grants'] =
+          taxonomy_access_grant_add_table($vocab_default, $vid);
+        $form[$name]['new'][$vid]['add'] = array(
+          '#type' => 'submit',
+          '#name' => $vid,
+          '#submit' => array('taxonomy_access_add_term_submit'),
+          '#value' => t('Add'),
+        );
+      }
+      $disable_url = url(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/disable/$vid",
+        array('query' => drupal_get_destination())
+      );
+      $form[$name]['disable'] = array(
+          '#markup' => '<p>' . t(
+            'To disable the %vocab vocabulary, <a href="@url">delete all @vocab access rules</a>.',
+            array('%vocab' => $vocab->name, '@vocab' => $vocab->name, '@url' => $disable_url)) . '</p>'
+      );
+    }
+  }
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save all'),
+    '#submit' => array('taxonomy_access_save_all_submit'),
+  );
+  if (!empty($term_grants)) {
+    $form['actions']['delete'] = array(
+      '#type' => 'submit',
+      '#value' => t('Delete selected'),
+      '#submit' => array('taxonomy_access_delete_selected_submit'),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Generates a grant table for multiple access rules.
+ *
+ * @param array $rows
+ *   An array of grant row data, keyed by an ID (term, vocab, role, etc.). Each
+ *   row should include the following keys:
+ *   - name: (optional) The label for the row (e.g., a term, vocabulary, or
+ *     role name).
+ *   - view: The View grant value select box for the element.
+ *   - update: The Update grant value select box for the element.
+ *   - delete: The Delete grant value select box for the element.
+ *   - create: The Add tag grant value select box for the element.
+ *   - list: The View tag grant value select box for the element.
+ * @param int $parent_vid
+ *   The parent ID for the table in the form tree structure (typically a
+ *   vocabulary id).
+ * @param string $first_col
+ *   The header for the first column (in the 'name' key for each row).
+ * @param bool $delete
+ *   (optional) Whether to add a deletion checkbox to each row along with a
+ *   "Check all" box in the table header. The checbox is automatically disabled
+ *   for TAXONOMY_ACCESS_VOCABULARY_DEFAULT. Defaults to TRUE.
+ *
+ * @return
+ *   Renderable array containing the table.
+ *
+ * @see taxonomy_access_grant_table()
+ */
+function taxonomy_access_grant_table(array $rows, $parent_vid, $first_col, $delete = TRUE) {
+  $header = taxonomy_access_grant_table_header();
+  if ($first_col) {
+    array_unshift(
+      $header,
+      array('data' => $first_col, 'class' => array('taxonomy-access-label'))
+    );
+  }
+  if ($delete) {
+    drupal_add_js('misc/tableselect.js');
+    array_unshift($header, array('class' => array('select-all')));
+  }
+  $table = array(
+    '#type' => 'taxonomy_access_grant_table',
+    '#tree' => TRUE,
+    '#header' => $header,
+  );
+  foreach ($rows as $id => $row) {
+    $table[$parent_vid][$id] = taxonomy_access_admin_build_row($row, 'name', $delete);
+  }
+  // Disable the delete checkbox for the default.
+  if ($delete && isset($table[$parent_vid][TAXONOMY_ACCESS_VOCABULARY_DEFAULT])) {
+    $table[$parent_vid][TAXONOMY_ACCESS_VOCABULARY_DEFAULT]['remove']['#disabled'] = TRUE;
+  }
+
+  return $table;
+}
+
+/**
+ * Generates a grant table for adding access rules with one set of values.
+ *
+ * @param array $rows
+ *   An associative array of access rule data, with the following keys:
+ *   - view: The View grant value select box for the element.
+ *   - update: The Update grant value select box for the element.
+ *   - delete: The Delete grant value select box for the element.
+ *   - create: The Add tag grant value select box for the element.
+ *   - list: The View tag grant value select box for the element.
+ * @param int $id
+ *   The ID for this set (e.g., a vocabulary ID).
+ *
+ * @return
+ *   Renderable array containing the table.
+ *
+ * @see taxonomy_access_grant_table()
+ */
+function taxonomy_access_grant_add_table($row, $id) {
+  $header = taxonomy_access_grant_table_header();
+  $table = array(
+    '#type' => 'taxonomy_access_grant_table',
+    '#tree' => TRUE,
+    '#header' => $header,
+  );
+  $table[$id][TAXONOMY_ACCESS_VOCABULARY_DEFAULT] = taxonomy_access_admin_build_row($row);
+
+  return $table;
+}
+
+/**
+ * Returns a header array for grant form tables.
+ *
+ * @return array
+ *   An array of header cell data for a grant table.
+ */
+function taxonomy_access_grant_table_header() {
+  $header = array(
+    array('data' => t('View')),
+    array('data' => t('Update')),
+    array('data' => t('Delete')),
+    array('data' => t('Add Tag')),
+    array('data' => t('View Tag')),
+  );
+  foreach ($header as &$cell) {
+    $cell['class'] = array('taxonomy-access-grant');
+  }
+  return $header;
+}
+
+/**
+ * Theme our grant table.
+ *
+ * @todo
+ *   Use a separate theme function for taxonomy_access_grant_add_table() to get
+ *   out of nesting hell?
+ * @todo
+ *   I clearly have no idea what I'm doing here.
+ */
+function theme_taxonomy_access_grant_table($element_data) {
+  $table = array();
+  $table['header'] = $element_data['elements']['#header'];
+  $table['attributes']['class'] = array('taxonomy-access-grant-table');
+  $rows = array();
+  foreach (element_children($element_data['elements']) as $element_key) {
+    $child = $element_data['elements'][$element_key];
+    foreach (element_children($child) as $child_key) {
+      $record = $child[$child_key];
+      $row = array();
+      foreach (element_children($record) as $record_key) {
+        $data = array('data' => $record[$record_key]);
+        // If it's the default, add styling.
+        if ($record_key == 'name') {
+          $data['class'] = array('taxonomy-access-label');
+          if ($child_key == TAXONOMY_ACCESS_VOCABULARY_DEFAULT) {
+            $data['class'][] = 'taxonomy-access-default';
+          }
+        }
+        // Add grant classes to grant cells.
+        elseif (in_array($record_key, array('view', 'update', 'delete', 'create', 'list'))) {
+          $grant_class = $record_key . '-' . $data['data']['#default_value'];
+          $data['class'] = array('taxonomy-access-grant', $grant_class);
+        }
+        $row[] = $data;
+      }
+      $rows[] = $row;
+    }
+  }
+  $table['rows'] = $rows;
+  return theme('table', $table);
+}
+
+/**
+ * Assembles a row of grant options for a term or default on the admin form.
+ *
+ * @param array $grants
+ *   An array of grants to use as form defaults.
+ * @param $label_key
+ *   (optional) Key of the column to use as a label in each grant row. Defaults
+ *   to NULL.
+ */
+function taxonomy_access_admin_build_row(array $grants, $label_key = NULL, $delete = FALSE) {
+  $form['#tree'] = TRUE;
+  if ($delete) {
+    $form['remove'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Delete access rule for @name', array('@name' => $grants[$label_key])),
+      '#title_display' => 'invisible',
+    );
+  }
+  if ($label_key) {
+    $form[$label_key] = array(
+      '#type' => 'markup',
+      '#markup' => check_plain($grants[$label_key]),
+    );
+  }
+  foreach (array('view', 'update', 'delete', 'create', 'list') as $grant) {
+    $for = $label_key ? $grants[$label_key] : NULL;
+    $form[$grant] = array(
+      '#type' => 'select',
+      '#title' => _taxonomy_access_grant_field_label($grant, $for),
+      '#title_display' => 'invisible',
+      '#default_value' => is_string($grants['grant_' . $grant]) ? $grants['grant_' . $grant] : TAXONOMY_ACCESS_NODE_IGNORE,
+      '#required' => TRUE,
+    );
+  }
+  foreach (array('view', 'update', 'delete') as $grant) {
+    $form[$grant]['#options'] = array(
+      TAXONOMY_ACCESS_NODE_ALLOW => t('Allow'),
+      TAXONOMY_ACCESS_NODE_IGNORE => t('Ignore'),
+      TAXONOMY_ACCESS_NODE_DENY => t('Deny'),
+    );
+  }
+  foreach (array('create', 'list') as $grant) {
+    $form[$grant]['#options'] = array(
+      TAXONOMY_ACCESS_TERM_ALLOW => t('Allow'),
+      TAXONOMY_ACCESS_TERM_DENY => t('Deny'),
+    );
+  }
+  return $form;
+}
+
+/**
+ * Returns the proper invisible field label for each grant table element.
+ */
+function _taxonomy_access_grant_field_label($grant, $for = NULL) {
+  if ($for) {
+    $label = array('@label', $for);
+    $titles = array(
+      'view' => t('View grant for @label', $label),
+      'update' => t('Update grant for @label', $label),
+      'delete' => t('Delete grant for @label', $label),
+      'create' => t('Add tag grant for @label', $label),
+      'list' => t('View tag grant for @label', $label),
+    );
+  }
+  else {
+    $titles = array(
+      'view' => t('View grant'),
+      'update' => t('Update grant'),
+      'delete' => t('Delete grant'),
+      'create' => t('Add tag grant'),
+      'list' => t('View tag grant'),
+    );
+  }
+
+ return $titles[$grant];
+}
+
+/**
+ * Form submission handler for taxonomy_access_admin_role().
+ *
+ * Processes submissions for the vocabulary 'Add' button.
+ */
+function taxonomy_access_enable_vocab_submit($form, &$form_state) {
+  $rid = $form_state['values']['rid'];
+  $vid = $form_state['values']['enable_vocab'];
+  $roles = _taxonomy_access_user_roles();
+  $vocab = taxonomy_vocabulary_load($vid);
+  if (taxonomy_access_enable_vocab($vid, $rid)) {
+    drupal_set_message(t(
+      'Vocabulary %vocab enabled successfully for the %role role.',
+      array(
+        '%vocab' => $vocab->name,
+        '%role' => $roles[$rid])));
+  }
+  else {
+    drupal_set_message(t('The vocabulary could not be enabled.'), 'error');
+  }
+}
+
+/**
+ * Form submission handler for taxonomy_access_admin_role().
+ *
+ * Processes submissions for the term 'Add' button.
+ */
+function taxonomy_access_add_term_submit($form, &$form_state) {
+  $vid = $form_state['clicked_button']['#name'];
+  $new = $form_state['values']['new'][$vid];
+  $rid = $form_state['values']['rid'];
+  list($type, $id) = explode(' ', $new['item']);
+  $rows = array();
+
+  $rows[$id] =
+    _taxonomy_access_format_grant_record($id, $rid, $new['grants'][$vid][TAXONOMY_ACCESS_VOCABULARY_DEFAULT]);
+
+  // If we are adding children recursively, add those as well.
+  if ($new['recursive'] == 1) {
+    $children = _taxonomy_access_get_descendants($id);
+    foreach ($children as $tid) {
+      $rows[$tid] =
+        _taxonomy_access_format_grant_record($tid, $rid, $new['grants'][$vid][TAXONOMY_ACCESS_VOCABULARY_DEFAULT]);
+    }
+  }
+
+  // Set the grants for the row or rows.
+  taxonomy_access_set_term_grants($rows);
+}
+
+/**
+ * Page callback: Returns a confirmation form to disable a vocabulary.
+ *
+ * @param int $rid
+ *   The role ID.
+ * @param object $vocab
+ *   The vocabulary object.
+ *
+ * @todo
+ *   Check if vocab is enabled and return a 404 otherwise?
+ *
+ * @see taxonomy_access_menu()
+ * @see taxonomy_access_admin_role().
+ * @see taxonomy_access_disable_vocab_confirm_page()
+ */
+function taxonomy_access_disable_vocab_confirm_page($rid, $vocab) {
+  $rid = intval($rid);
+
+  // Return a 404 on invalid vid or rid.
+  if (!$vocab->vid || !$rid) {
+    return MENU_NOT_FOUND;
+  }
+
+  return drupal_get_form('taxonomy_access_disable_vocab_confirm', $rid, $vocab);
+}
+
+/**
+ * Returns a confirmation form for disabling a vocabulary for a role.
+ *
+ * @param int $rid
+ *   The role ID.
+ * @param object $vocab
+ *   The vocabulary object.
+ *
+ * @see taxonomy_access_disable_vocab_confirm_page()
+ * @see taxonomy_access_disable_vocab_confirm_submit()
+ * @ingroup forms
+ */
+function taxonomy_access_disable_vocab_confirm($form, &$form_state, $rid, $vocab) {
+  $roles = _taxonomy_access_user_roles();
+  if (taxonomy_access_role_enabled($rid)) {
+    $form['rid'] = array(
+      '#type' => 'value',
+      '#value' => $rid,
+    );
+    $form['vid'] = array(
+      '#type' => 'value',
+      '#value' => $vocab->vid,
+    );
+    $form['vocab_name'] = array(
+      '#type' => 'value',
+      '#value' => $vocab->name,
+    );
+    return confirm_form($form,
+      t("Are you sure you want to delete all Taxonomy access rules for %vocab in the %role role?",
+        array('%role' => $roles[$rid], '%vocab' => $vocab->name)),
+      TAXONOMY_ACCESS_CONFIG . '/role/%/edit',
+      t('This action cannot be undone.'),
+      t('Delete all'),
+      t('Cancel'));
+  }
+}
+
+/**
+ * Form submission handler for taxonomy_access_disable_vocab_confirm().
+ *
+ * @param int $rid
+ *   The role ID to disable.
+ *
+ * @todo
+ *   Set a message on invalid $rid or $vid?
+ */
+function taxonomy_access_disable_vocab_confirm_submit($form, &$form_state) {
+  $roles = _taxonomy_access_user_roles();
+  $rid = intval($form_state['values']['rid']);
+  $vid = intval($form_state['values']['vid']);
+  // Do not proceed for invalid role IDs, and do not allow the global default
+  // to be deleted.
+  if (!$vid || !$rid || empty($roles[$rid])) {
+    return FALSE;
+  }
+
+  if ($form_state['values']['confirm']) {
+    $form_state['redirect'] = TAXONOMY_ACCESS_CONFIG;
+    if (taxonomy_access_disable_vocab($vid, $rid)) {
+      drupal_set_message(
+        t('All Taxonomy access rules deleted for %vocab in role %role.',
+          array(
+            '%vocab' => $form_state['values']['vocab_name'],
+            '%role' => $roles[$rid])
+         ));
+      return TRUE;
+    }
+  }
+}
+
+/**
+ * Form submission handler for taxonomy_access_admin_role().
+ *
+ * Processes submissions for the "Delete selected" button.
+ *
+ * @todo
+ *   The parent form could probably be refactored to make this more efficient
+ *   (by putting these elements in a flat list) but that would require changing
+ *   taxonomy_access_grant_table() and taxonomy_access_build_row().
+ */
+function taxonomy_access_delete_selected_submit($form, &$form_state) {
+  $rid = intval($form_state['values']['rid']);
+  $delete_tids = array();
+  foreach ($form_state['values']['grants'] as $vid => $tids) {
+    foreach ($tids as $tid => $record) {
+      if (!empty($record['remove'])) {
+        $delete_tids[] = $tid;
+      }
+    }
+  }
+  if ($rid) {
+    if (taxonomy_access_delete_term_grants($delete_tids, $rid)) {
+      drupal_set_message(format_plural(
+          sizeof($delete_tids),
+          '1 term access rule was deleted.',
+          '@count term access rules were deleted.'));
+    }
+    else {
+      drupal_set_message(t('The records could not be deleted.'), 'warning');
+    }
+  }
+}
+/**
+ * Form submission handler for taxonomy_access_admin_form().
+ *
+ * Processes submissions for the 'Save all' button.
+ */
+function taxonomy_access_save_all_submit($form, &$form_state) {
+  $values = $form_state['values'];
+  $rid = $values['rid'];
+  $vocabs = taxonomy_get_vocabularies();
+
+  // Create four lists of records to update.
+  $update_terms = array();
+  $skip_terms = array();
+  $update_defaults = array();
+  $skip_defaults = array();
+
+  foreach ($values['grants'] as $vid => $rows) {
+    if ($vid == TAXONOMY_ACCESS_GLOBAL_DEFAULT) {
+      $element = $form['global_default'];
+    }
+    else {
+      $vocab = $vocabs[$vid];
+      $element = $form[$vocab->machine_name];
+    }
+    foreach ($rows as $tid => $row) {
+      // Check the default values for this row.
+      $defaults = array();
+      $grants = array();
+      foreach (array('view', 'update', 'delete', 'create', 'list') as $grant_name) {
+        $grants[$grant_name] = $row[$grant_name];
+        $defaults[$grant_name] =
+          $element['grants'][$vid][$tid][$grant_name]['#default_value'];
+      }
+
+      // Proceed if the user changed the row (values differ from defaults).
+      if ($defaults != $grants) {
+        // If the grants for node access match the defaults, then we
+        // can skip updating node access records for this row.
+        $update_nodes = FALSE;
+        foreach (array('view', 'update', 'delete') as $op) {
+          if ($defaults[$op] != $grants[$op]) {
+            $update_nodes = TRUE;
+          }
+        }
+
+        // Add the row to one of the four arrays.
+        switch (TRUE) {
+          // Term record with node grant changes.
+          case ($tid && $update_nodes):
+            $update_terms[$tid] =
+              _taxonomy_access_format_grant_record($tid, $rid, $grants);
+            break;
+
+          // Term record and no node grant changes.
+          case ($tid && !$update_nodes):
+            $skip_terms[$tid] =
+              _taxonomy_access_format_grant_record($tid, $rid, $grants);
+            break;
+
+          // Vocab record with node grant changes.
+          case (!$tid && $update_nodes):
+            $update_defaults[$vid] =
+              _taxonomy_access_format_grant_record($vid, $rid, $grants, TRUE);
+            break;
+
+          // Vocab record and no node grant changes.
+          case (!$tid && !$update_nodes):
+            $skip_defaults[$vid] =
+              _taxonomy_access_format_grant_record($vid, $rid, $grants, TRUE);
+            break;
+        }
+      }
+    }
+  }
+
+  // Process each set.
+  if (!empty($update_terms)) {
+    taxonomy_access_set_term_grants($update_terms);
+  }
+  if (!empty($skip_terms)) {
+    taxonomy_access_set_term_grants($skip_terms, FALSE);
+  }
+  if (!empty($update_defaults)) {
+    taxonomy_access_set_default_grants($update_defaults);
+  }
+  if (!empty($skip_defaults)) {
+    taxonomy_access_set_default_grants($skip_defaults, FALSE);
+  }
+}
+
+
+/**
+ * Generates HTML markup with form instructions for the admin form.
+ *
+ * @return
+ *   Instructions HTML string.
+ */
+function _taxonomy_access_admin_instructions_html() {
+  $instructions = '';
+  $instructions .= ''
+    . "<br /><br />"
+    . "<div class=\"instructions\">"
+    . "<h2>" . t("Explanation of fields") . "</h2>"
+    . _taxonomy_access_grant_help_table()
+    . "<p>"
+    . t('Options for View, Update, and Delete are <em>Allow</em> (<acronym title="Allow">A</acronym>), <em>Ignore</em> (<acronym title="Ignore">I</acronym>), and <em>Deny</em> (<acronym title="Deny">D</acronym>).')
+    . "</p>\n\n"
+    . "<ul>\n"
+    . "<li>"
+    . t('<em>Deny</em> (<acronym title="Deny">D</acronym>) overrides <em>Allow</em> (<acronym title="Allow">A</acronym>) within this role.')
+    . "</li>"
+    . "<li>"
+    . t('Both <em>Allow</em> (<acronym title="Allow">A</acronym>) and <em>Deny</em> (<acronym title="Deny">D</acronym>) override <em>Ignore</em> (<acronym title="Ignore">I</acronym>) within this role.')
+    . "</li>"
+    . "<li>"
+    . t('If a user has <strong>multiple roles</strong>, an <em>Allow</em> (<acronym title="Allow">A</acronym>) from another role <strong>will</strong> override a <em>Deny</em> (<acronym title="Deny">D</acronym>) here.')
+    . "</li>"
+    . "</ul>\n\n"
+    ;
+  if (arg(4) != DRUPAL_ANONYMOUS_RID && arg(4) != DRUPAL_AUTHENTICATED_RID) {
+    // Role other than Anonymous or Authenticated
+    $instructions .= ''
+      . "<p>"
+      . t('<strong>Remember:</strong> This role <strong>will</strong> inherit permissions from the <em>authenticated user</em> role.  Be sure to <a href="@url">configure the authenticated user</a> properly.',
+        array("@url" => url(
+            TAXONOMY_ACCESS_CONFIG
+            . "/role/"
+            .  DRUPAL_AUTHENTICATED_RID
+            . '/edit')))
+      . "</p>\n\n"
+      ;
+  }
+  $instructions .= ''
+    . "<p>"
+    . t('For more information and for troubleshooting guidelines, see the <a href="@help">help page</a> and the !readme.',
+      array(
+        '@help' => url('admin/help/taxonomy_access'),
+        '!readme' => "<code>README.txt</code>"
+      ))
+    . "</p>\n\n"
+    . "</div>\n\n"
+    ;
+
+  return $instructions;
+
+}

+ 873 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.create.inc

@@ -0,0 +1,873 @@
+<?php
+
+/**
+ * @file
+ * Implements the Add Tag (create) grant on editing forms.
+ *
+ * These functions need to be included in three circumstances:
+ * - Form building for forms with taxonomy fields.
+ *   - taxonomy_access_field_widget_form_alter()
+ *   - taxonomy_access_field_widget_taxonomy_autocomplete_form_alter()
+ * - Form validation for forms with taxonomy fields.
+ *   - taxonomy_access_autocomplete_validate()
+ *   - taxonomy_access_options_validate()
+ *   - taxonomy_access_field_attach_validate()
+ * - Taxonomy autocomplete AJAX requests.
+ *   - taxonomy_access_autocomplete()
+ */
+
+/**
+ * @defgroup tac_create Taxonomy Access Control: Add tag (create) permission
+ * @{
+ * Implement access control for taxonomy terms on node editing forms.
+ */
+
+/**
+ * Implements the create grant for autocomplete fields.
+ *
+ * - Denies access if the user cannot alter the field values.
+ * - Determines whether the user can autocreate new terms for the field.
+ * - Removes default values disallowed by create.
+ * - Adds information on autocreate and disallowed defaults to the element so
+ *   it is available to the validator.
+ * - Adds the custom validator.
+ * - Sets a custom autocomplete path to filter autocomplete by create.
+ *
+ * Some of the logic here is borrowed from taxonomy_autocomplete_validate().
+ *
+ * @see taxonomy_access_field_widget_taxonomy_autocomplete_form_alter()
+ */
+function _taxonomy_access_autocomplete_alter(&$element, &$form_state, $context) {
+
+  // Collect a list of terms and filter out those disallowed by create.
+  $filtered = array();
+  foreach ($context['items'] as $item) {
+    $filtered[$item['tid']] = $item;
+  }
+  $disallowed_defaults = taxonomy_access_create_disallowed(array_keys($filtered));
+  foreach ($disallowed_defaults as $tid) {
+    unset($filtered[$tid]);
+  }
+
+  // Assemble a list of all vocabularies for the field.
+  $vids = array();
+  foreach ($context['field']['settings']['allowed_values'] as $tree) {
+    if ($vocab = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
+      $vids[] = $vocab->vid;
+    }
+  }
+
+  // Determine whether the user has create for any terms in the given vocabs.
+  $allowed_terms = FALSE;
+  foreach ($vids as $vid) {
+    $terms = taxonomy_access_user_create_terms_by_vocab($vid);
+    if (!empty($terms)) {
+      $allowed_terms = TRUE;
+      break;
+    }
+  }
+
+  // Filter the vids to vocabs in which the user may create new terms.
+  $allowed_vids = taxonomy_access_create_default_allowed($vids);
+
+  // If the field already has the maximum number of values, and all of these
+  // values are disallowed, deny access to the field.
+  if ($context['field']['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
+    if (sizeof($disallowed_defaults) >= $context['field']['cardinality']) {
+      $element['#access'] = FALSE;
+    }
+  }
+
+  // If the user may not create any terms on this field, deny access.
+  if (empty($allowed_vids) && !$allowed_terms) {
+    $element['#access'] = FALSE;
+  }
+
+  // Set the default value from the filtered item list.
+  $element['#default_value'] =
+    taxonomy_access_autocomplete_default_value($filtered);
+
+  // Custom validation.  Set values for the validator indicating:
+  // 1. Whether the user can autocreate terms in this field (vocab. default).
+  // 2. Which tids were removed due to create restrictions.
+  $element['#allow_autocreate'] = empty($allowed_vids) ? FALSE : TRUE;
+  $element['#disallowed_defaults'] = $disallowed_defaults;
+  $element['#element_validate'] =
+    array('taxonomy_access_autocomplete_validate');
+
+  // Use a custom autocomplete path to filter by create rather than list.
+  $element['#autocomplete_path'] =
+    'taxonomy_access/autocomplete/' . $context['field']['field_name'];
+
+  unset($context);
+}
+
+
+/**
+ * Implements the create grant for options widgets.
+ *
+ * - Denies access if the user cannot alter the field values.
+ * - Attaches jQuery to disable values disallowed by create.
+ * - Adds the disallowed values from the element so they are available to the
+ *   custom validator.
+ * - Adds the custom validator.
+ *
+ * We use jQuery to disable the options because of FAPI limitations:
+ * @see http://drupal.org/node/284917
+ * @see http://drupal.org/node/342316
+ * @see http://drupal.org/node/12089
+ *
+ * @see taxonomy_access_field_widget_form_alter()
+ */
+function _taxonomy_access_options_alter(&$element, &$form_state, $context) {
+
+  // Check for an existing entity ID.
+  $entity_id = 0;
+  if (!empty($form_state['build_info']['args'][0])) {
+    $info = entity_get_info($context['instance']['entity_type']);
+    $pseudo_entity = (object) $form_state['build_info']['args'][0];
+    if (isset($pseudo_entity->{$info['entity keys']['id']})) {
+      $entity_id = $pseudo_entity->{$info['entity keys']['id']};
+    }
+  }
+  // Collect a list of terms and determine which are allowed
+  $tids = array_keys($element['#options']);
+
+  // Ignore the "none" option if present.
+  $key = array_search('_none', $tids);
+  if ($key !== FALSE) {
+    unset($tids[$key]);
+  }
+
+  $allowed_tids = taxonomy_access_create_allowed($tids);
+  $disallowed_tids = taxonomy_access_create_disallowed($tids);
+
+  // If no options are allowed, deny access to the field.
+  if (empty($allowed_tids)) {
+    $element['#access'] = FALSE;
+  }
+
+  // On node creation, simply remove disallowed default values.
+  if (!$entity_id) {
+    $disallowed_defaults = array();
+    if (is_array($element['#default_value'])) {
+      foreach ($element['#default_value'] as $key => $value) {
+        if (in_array($value, $disallowed_tids)) {
+          unset($element['#default_value'][0]);
+        }
+      }
+    }
+    elseif (in_array($element['#default_value'], $disallowed_tids)) {
+      unset($element['#default_value']);
+    }
+  }
+  // If the node already exists, check:
+  // 1. Whether the field already has the maximum number of values
+  // 2. Whether all of these values are disallowed.
+  // If both these things are true, then the user cannot edit the field's
+  // value, so disallow access.
+  else {
+    $defaults =
+      is_array($element['#default_value'])
+      ? $element['#default_value']
+      : array($element['#default_value'])
+      ;
+
+    $disallowed_defaults =
+      array_intersect($defaults, $disallowed_tids);
+
+    if ($context['field']['cardinality'] != FIELD_CARDINALITY_UNLIMITED) {
+      if (sizeof($disallowed_defaults) >= $context['field']['cardinality']) {
+        $element['#access'] = FALSE;
+      }
+    }
+  }
+
+  // If there are disallowed, terms, add CSS and JS for jQuery.
+  // We use jQuery because FAPI does not currently support attributes
+  // for individual options.
+  if (!empty($disallowed_tids)) {
+
+    // Add a css class to the field that we can use in jQuery.
+    $class_name = 'tac_' . $element['#field_name'];
+    $element['#attributes']['class'][] = $class_name;
+
+    // Add js for disabling create options.
+    $settings[] = array(
+      'field' => $class_name,
+      'disallowed_tids' => $disallowed_tids,
+      'disallowed_defaults' => $disallowed_defaults,
+    );
+    $element['#attached']['js'][] =
+      drupal_get_path('module', 'taxonomy_access') . '/tac_create.js';
+    $element['#attached']['js'][] = array(
+      'data' => array('taxonomy_access' => $settings),
+      'type' => 'setting',
+    );
+  }
+
+  $element['#disallowed_defaults'] = $disallowed_defaults;
+  $element['#element_validate'] = array('taxonomy_access_options_validate');
+}
+
+/**
+ * Retrieve terms that the current user may create.
+ *
+ * @return array|true
+ *   An array of term IDs, or TRUE if the user may create all terms.
+ *
+ * @see taxonomy_access_user_create_terms_by_vocab()
+ * @see _taxonomy_access_user_term_grants()
+ */
+function taxonomy_access_user_create_terms() {
+  // Cache the terms the current user can create.
+  $terms = &drupal_static(__FUNCTION__, NULL);
+  if (is_null($terms)) {
+    $terms = _taxonomy_access_user_term_grants(TRUE);
+  }
+  return $terms;
+}
+
+/**
+ * Retrieve terms that the current user may create in specific vocabularies.
+ *
+ * @param int $vid
+ *   A vid to use as a filter.
+ *
+ * @return array|true
+ *   An array of term IDs, or TRUE if the user may create all terms.
+ *
+ * @see taxonomy_access_user_create_terms()
+ * @see _taxonomy_access_user_term_grants()
+ */
+function taxonomy_access_user_create_terms_by_vocab($vid) {
+  // Cache the terms the current user can create per vocabulary.
+  static $terms = array();
+  if (!isset($terms[$vid])) {
+    $terms[$vid] = _taxonomy_access_user_term_grants(TRUE, array($vid));
+  }
+  return $terms[$vid];
+}
+
+/**
+ * Retrieve terms that the current user may create.
+ *
+ * @return array|true
+ *   An array of term IDs, or TRUE if the user may create all terms.
+ *
+ * @see _taxonomy_access_create_defaults()
+ */
+function taxonomy_access_user_create_defaults() {
+  // Cache the terms the current user can create.
+  static $vids = NULL;
+  if (is_null($vids)) {
+    $vids = _taxonomy_access_create_defaults();
+  }
+  return $vids;
+}
+
+/**
+ * Check a list of term IDs for terms the user may not create.
+ *
+ * @param array $tids
+ *   The array of term IDs.
+ *
+ * @return array
+ *   An array of disallowed term IDs.
+ */
+function taxonomy_access_create_disallowed(array $tids) {
+  $all_allowed = taxonomy_access_user_create_terms();
+
+  // If the user's create grant info is exactly TRUE, no terms are disallowed.
+  if ($all_allowed === TRUE) {
+    return array();
+  }
+
+  return array_diff($tids, $all_allowed);
+}
+
+/**
+ * Filter a list of term IDs to terms the user may create.
+ *
+ * @param array $tids
+ *   The array of term IDs.
+ *
+ * @return array
+ *   An array of disallowed term IDs.
+ */
+function taxonomy_access_create_allowed(array $tids) {
+  $all_allowed = taxonomy_access_user_create_terms();
+
+  // If the user's create grant info is exactly TRUE, all terms are allowed.
+  if ($all_allowed === TRUE) {
+    return $tids;
+  }
+
+  return array_intersect($tids, $all_allowed);
+}
+
+/**
+ * Filter a list of vocab IDs to those in which the user may create by default.
+ *
+ * @param array $vids
+ *   The array of vocabulary IDs.
+ *
+ * @return array
+ *   An array of disallowed vocabulary IDs.
+ */
+function taxonomy_access_create_default_allowed(array $vids) {
+  $all_allowed = taxonomy_access_user_create_defaults();
+
+  // If the user's create grant info is exactly TRUE, all terms are allowed.
+  if ($all_allowed === TRUE) {
+    return $vids;
+  }
+
+  return array_intersect($vids, $all_allowed);
+}
+
+
+/**
+ * Retrieve vocabularies in which the current user may create terms.
+ *
+ * @param object|null $account
+ *   (optional) The account for which to retrieve grants.  If no account is
+ *   passed, the current user will be used.  Defaults to NULL.
+ *
+ * @return array
+ *   An array of term IDs, or TRUE if the user has the grant for all terms.
+ */
+function _taxonomy_access_create_defaults($account = NULL) {
+
+  // If the user can administer taxonomy, return TRUE for a global grant.
+  if (user_access('administer taxonomy', $account)) {
+    return TRUE;
+  }
+
+  // Build a term grant query.
+  $query = _taxonomy_access_grant_query(array('create'), TRUE);
+
+  // Select term grants for the current user's roles.
+  if (is_null($account)) {
+    global $user;
+    $account = $user;
+  }
+  $query
+    ->fields('td', array('vid'))
+    ->groupBy('td.vid')
+    ->condition('tadg.rid', array_keys($account->roles), 'IN')
+    ;
+
+  // Fetch term IDs.
+  $r = $query->execute()->fetchAll();
+  $vids = array();
+
+  // If there are results, initialize a flag to test whether the user
+  // has the grant for all terms.
+  $grants_for_all_vocabs = empty($r) ? FALSE : TRUE;
+
+  foreach ($r as $record) {
+    // If the user has the grant, add the term to the array.
+    if ($record->grant_create) {
+      $vids[] = $record->vid;
+    }
+    // Otherwise, flag that the user does not have the grant for all terms.
+    else {
+      $grants_for_all_vocabs = FALSE;
+    }
+  }
+
+  // If the user has the grant for all terms, return TRUE for a global grant.
+  if ($grants_for_all_vocabs) {
+    return TRUE;
+  }
+
+  return $vids;
+}
+
+/**
+ * Autocomplete menu callback: filter allowed terms by create, not list.
+ *
+ * For now we essentially duplicate the code from taxonomy.module, because
+ * it calls drupal_json_output without providing the logic separately.
+ *
+ * @see http://drupal.org/node/1169964
+ * @see taxonomy_autocomplete()
+ */
+function taxonomy_access_autocomplete($field_name, $tags_typed = '') {
+  // Enforce that list grants do not filter the autocomplete.
+  taxonomy_access_disable_list();
+
+  $field = field_info_field($field_name);
+
+  // The user enters a comma-separated list of tags. We only autocomplete the last tag.
+  $tags_typed = drupal_explode_tags($tags_typed);
+  $tag_last = drupal_strtolower(array_pop($tags_typed));
+
+  $matches = array();
+  if ($tag_last != '') {
+
+    // Part of the criteria for the query come from the field's own settings.
+    $vids = array();
+    $vocabularies = taxonomy_vocabulary_get_names();
+    foreach ($field['settings']['allowed_values'] as $tree) {
+      $vids[] = $vocabularies[$tree['vocabulary']]->vid;
+    }
+
+    $query = db_select('taxonomy_term_data', 't');
+    $query->addTag('translatable');
+    $query->addTag('term_access');
+
+    // Do not select already entered terms.
+    if (!empty($tags_typed)) {
+      $query->condition('t.name', $tags_typed, 'NOT IN');
+    }
+    // Select rows that match by term name.
+    $tags_return = $query
+      ->fields('t', array('tid', 'name'))
+      ->condition('t.vid', $vids)
+      ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE')
+      ->range(0, 10)
+      ->execute()
+      ->fetchAllKeyed();
+
+    // Unset suggestions disallowed by create grants.
+    $disallowed = taxonomy_access_create_disallowed(array_keys($tags_return));
+    foreach ($disallowed as $tid) {
+      unset($tags_return[$tid]);
+    }
+
+    $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
+
+    $term_matches = array();
+    foreach ($tags_return as $tid => $name) {
+      $n = $name;
+      // Term names containing commas or quotes must be wrapped in quotes.
+      if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
+        $n = '"' . str_replace('"', '""', $name) . '"';
+      }
+      $term_matches[$prefix . $n] = check_plain($name);
+    }
+  }
+
+  drupal_json_output($term_matches);
+}
+
+/**
+ * Validates taxonomy autocomplete values for create grants.
+ *
+ * For now we essentially duplicate the code from taxonomy.module, because
+ * it calls form_set_value() without providing the logic separately.
+ *
+ * We use two properties set in hook_field_widget_form_alter():
+ *  - $element['#allow_autocreate']
+ *  - $element['#disallowed_defaults']
+ *
+ * @todo
+ *   Specify autocreate per vocabulary?
+ *
+ * @see taxonomy_autocomplete_validate()
+ * @see taxonomy_access_autocomplete()
+ * @see taxonomy_access_field_widget_taxonomy_autocomplete_form_alter()
+ */
+function _taxonomy_access_autocomplete_validate($element, &$form_state) {
+  // Autocomplete widgets do not send their tids in the form, so we must detect
+  // them here and process them independently.
+  $value = array();
+  if ($tags = $element['#value']) {
+    // Collect candidate vocabularies.
+    $field = field_widget_field($element, $form_state);
+    $vocabularies = array();
+    foreach ($field['settings']['allowed_values'] as $tree) {
+      if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
+        $vocabularies[$vocabulary->vid] = $vocabulary;
+      }
+    }
+
+    // Translate term names into actual terms.
+    $typed_terms = drupal_explode_tags($tags);
+    foreach ($typed_terms as $typed_term) {
+      // See if the term exists in the chosen vocabulary and return the tid;
+      // otherwise, create a new 'autocreate' term for insert/update.
+      if ($possibilities = taxonomy_term_load_multiple(array(), array('name' => trim($typed_term), 'vid' => array_keys($vocabularies)))) {
+        $term = array_pop($possibilities);
+      }
+      // Only autocreate if the user has create for the vocab. default.
+      elseif ($element['#allow_autocreate']) {
+        $vocabulary = reset($vocabularies);
+        $term = array(
+          'tid' => 'autocreate',
+          'vid' => $vocabulary->vid,
+          'name' => $typed_term,
+          'vocabulary_machine_name' => $vocabulary->machine_name,
+        );
+      }
+      // If they cannot autocreate and this is a new term, set an error.
+      else {
+        form_error(
+          $element,
+          t('You may not create new tags in %name.',
+            array('%name' => t($element['#title']))
+          )
+        );
+      }
+      if ($term) {
+        $value[] = (array) $term;
+      }
+    }
+  }
+
+  // Add in the terms that were disallowed.
+  // taxonomy.module expects arrays, not objects.
+  $disallowed = taxonomy_term_load_multiple($element['#disallowed_defaults']);
+  foreach ($disallowed as $key => $term) {
+    $disallowed[$key] = (array) $term;
+  }
+  $value = array_merge($value, $disallowed);
+
+  // Subsequent validation will be handled by hook_field_attach_validate().
+  // Set the value in the form.
+  form_set_value($element, $value, $form_state);
+}
+
+/**
+ * Form element validation handler for taxonomy option fields.
+ *
+ * We use a property set in hook_field_widget_form_alter():
+ *  - $element['#disallowed_defaults']
+ *
+ * @see options_field_widget_validate()
+ * @see taxonomy_access_field_widget_form_alter()
+ */
+function _taxonomy_access_options_validate($element, &$form_state) {
+  if ($element['#required'] && $element['#value'] == '_none') {
+    form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
+  }
+
+  // Clone the element and add in disallowed defaults.
+  $el = $element;
+  if (!empty($element['#disallowed_defaults'])) {
+    if (empty($el['#value'])) {
+      $el['#value'] = $element['#disallowed_defaults'];
+    }
+    elseif (is_array($el['#value'])) {
+      $el['#value'] = array_unique(array_merge($el['#value'], $element['#disallowed_defaults']));
+    }
+    else {
+      $el['#value'] = array_unique(array_merge(array($el['#value']), $element['#disallowed_defaults']));
+    }
+  }
+
+  // Transpose selections from field => delta to delta => field, turning
+  // multiple selected options into multiple parent elements.
+  $items = _options_form_to_storage($el);
+
+  // Subsequent validation will be handled by hook_field_attach_validate().
+  // Set the value in the form.
+  form_set_value($element, $items, $form_state);
+}
+
+/**
+ * Default value re-generation for autocomplete fields.
+ *
+ * @param array $items
+ *   An array of values from form build info, filtered by create grants.
+ *
+ * @return string
+ *   Field default value.
+ *
+ * @see taxonomy_field_widget_form()
+ */
+function taxonomy_access_autocomplete_default_value(array $items) {
+  // Preserve the original state of the list flag.
+  $flag_state = taxonomy_access_list_enabled();
+
+  // Enforce that list grants do not filter the options list.
+  taxonomy_access_disable_list();
+
+  // Assemble list of tags.
+  $tags = array();
+  foreach ($items as $item) {
+    $tags[$item['tid']] = isset($item['taxonomy_term']) ? $item['taxonomy_term'] : taxonomy_term_load($item['tid']);
+  }
+
+  // Assemble the default value using taxonomy.module.
+  $tags = taxonomy_implode_tags($tags);
+
+  // Restore list flag to previous state.
+  if ($flag_state) {
+    taxonomy_access_enable_list();
+  }
+
+  return $tags;
+}
+
+/**
+ * Validates form submissions of taxonomy fields for create grants.
+ *
+ * @todo
+ *   Use field label and term names in errors rather than field name and tids.
+ *
+ * @see http://drupal.org/node/1220212
+ * @see entity_form_field_validate()
+ */
+function _taxonomy_access_field_validate($entity_type, $entity, &$errors) {
+  // Check for a pre-existing entity (i.e., the entity is being updated).
+  $old_fields = FALSE;
+
+  // The entity is actually a "pseudo-entity," and the user profile form
+  // neglects to include the uid. So, we need to load it manually.
+  if ($entity_type == 'user') {
+    // Some modules which extend the user profile form cause additional
+    // validation to happen with "pseudo-entities" that do not include the
+    // name. So, check if it exists.
+    if (isset($entity->name)) {
+      if ($account = user_load_by_name($entity->name)) {
+        $entity->uid = $account->uid;
+      }
+    }
+  }
+
+  list($entity_id, , $bundle) = entity_extract_ids($entity_type, $entity);
+  if ($entity_id) {
+    // Load the entity.
+    $old_entity = entity_load($entity_type, array($entity_id));
+    $old_entity = $old_entity[$entity_id];
+
+    // Fetch the original entity's taxonomy fields.
+    $old_fields =
+      _taxonomy_access_entity_fields($entity_type, $old_entity, $bundle);
+  }
+
+  // Fetch the updated entity's taxonomy fields.
+  $new_fields =
+    _taxonomy_access_entity_fields($entity_type, $entity, $bundle);
+
+  // Set errors if there are any disallowed changes.
+  $changes = _taxonomy_access_compare_fields($new_fields, $old_fields);
+
+  // We care about the overall value list, so delta is not important.
+  $delta = 0;
+
+  // Check each field and langcode for disallowed changes.
+  foreach ($changes as $field_name => $langcodes) {
+    foreach ($langcodes as $langcode => $disallowed) {
+      if ($disallowed) {
+        if (!empty($disallowed['added'])) {
+          $text = 'You may not add the following tags to %field: %tids';
+          $errors[$field_name][$langcode][$delta][] = array(
+            'error' => 'taxonomy_access_disallowed_added',
+            'message' => t($text, array(
+              '%field' => $field_name,
+              '%tids' => implode(', ', $disallowed['added']),
+            )),
+          );
+        }
+        if (!empty($disallowed['removed'])) {
+          $text = 'You may not remove the following tags from %field: %tids';
+          $errors[$field_name][$langcode][$delta][] = array(
+            'error' => 'taxonomy_access_disallowed_removed',
+            'message' => t($text, array(
+              '%field' => $field_name,
+              '%tids' => implode(', ', $disallowed['removed']),
+            )),
+          );
+        }
+      }
+    }
+  }
+}
+
+
+/**
+ * Helper function to extract the taxonomy fields from an entity.
+ *
+ * @param object $entity
+ *   The entity object.
+ *
+ * @return array
+ *   An associative array of field information, containing:
+ *   - field_list: A flat array of all this entity's taxonomy fields, with the
+ *     format $field_name => $field_name.
+ *   - langcodes: A flat array of all langcodes in this entity's fields, with
+ *     the format $langcode => $langcode.
+ *   - data: An associative array of non-empty fields:
+ *     - $field_name: An associative array keyed by langcode.
+ *       - $langcode: Array of field values for this field name and langcode.
+ *
+ * @see http://drupal.org/node/1220168
+ */
+function _taxonomy_access_entity_fields($entity_type, $entity, $bundle) {
+  // Maintain separate lists of field names and langcodes for quick comparison.
+  $fields = array();
+  $fields['field_list'] = array();
+  $fields['langcodes'] = array();
+  $fields['data'] = array();
+
+  // If there is no entity, return the empty structure.
+  if (!$entity) {
+    return $fields;
+  }
+
+  // Get a list of possible fields for the bundle.
+  // The bundle does not contain the field type (see #122016), so our only use
+  // for it is the field names.
+  $possible = array_keys(field_info_instances($entity_type, $bundle));
+
+  // Sort through the entity for relevant field data.
+  foreach ($entity as $field_name => $field) {
+
+    // Only proceed if this element is a valid field for the bundle.
+    if (in_array($field_name, $possible, TRUE)) {
+
+      // Check whether each entity field is a taxonomy field.
+      $info = field_info_field($field_name);
+      if ($info['type'] == 'taxonomy_term_reference') {
+        foreach ($field as $langcode => $values) {
+
+          // Add non-empty fields to the lists.
+          if (!empty($values)) {
+            $fields['langcodes'][$langcode] = $langcode;
+            $fields['field_list'][$field_name] = $field_name;
+            $fields['data'][$field_name][$langcode] = $values;
+          }
+          unset($values);
+        }
+      }
+    }
+    unset($info);
+    unset($field);
+  }
+
+  unset($entity);
+
+  return $fields;
+}
+
+/**
+ * Helper function to compare field values and look for disallowed changes.
+ *
+ * @param array $new
+ *   An associative array of the updated field information as returned by
+ *   _taxonomy_access_entity_fields().
+ * @param array $old
+ *   (optional) An associative array of the original field information,
+ *   or FALSE if there is no original field data.  Defaults to FALSE.
+ *
+ * @return array
+ *   An array of disallowed changes, with the structure:
+ *   - $field_name: An associative array keyed by langcode.
+ *     - $langcode: Disallowed changes for this field name and langcode,
+ *       or FALSE if none.
+ *       - 'added' => An array of added terms that are disallowed.
+ *       - 'removed' => An array of removed termss that are disallowed.
+ *
+ * @see _taxonomy_access_entity_fields()
+ * @see _taxonomy_access_disallowed_changes()
+ */
+function _taxonomy_access_compare_fields($new, $old = FALSE) {
+  $disallowed_changes = array();
+
+  // If there are no original fields, simply process new.
+  if (!$old) {
+    foreach ($new['data'] as $field_name => $langcodes) {
+      foreach ($langcodes as $langcode => $values) {
+        $changes = _taxonomy_access_disallowed_changes($values, array());
+        if ($changes) {
+          if (!isset($disallowed_changes[$field_name])) {
+            $disallowed_changes[$field_name] = array();
+          }
+          $disallowed_changes[$field_name][$langcode] = $changes;
+        }
+      }
+    }
+  }
+
+  // Otherwise, aggregate and compare field data.
+  else {
+    $all_fields = $new['field_list'] + $old['field_list'];
+    $all_langcodes = $new['langcodes'] + $old['langcodes'];
+
+    foreach ($all_fields as $field_name) {
+      foreach ($all_langcodes as $langcode) {
+        $new_values = array();
+        if (isset($new['field_list'][$field_name])
+          && isset($new['data'][$field_name][$langcode])) {
+          $new_values = $new['data'][$field_name][$langcode];
+        }
+        $old_values = array();
+        if (isset($old['field_list'][$field_name])
+          && isset($old['data'][$field_name][$langcode])) {
+          $old_values = $old['data'][$field_name][$langcode];
+        }
+        $changes = _taxonomy_access_disallowed_changes($new_values, $old_values);
+        if ($changes) {
+          if (!isset($disallowed_changes[$field_name])) {
+            $disallowed_changes[$field_name] = array();
+          }
+          $disallowed_changes[$field_name][$langcode] = $changes;
+        }
+      }
+    }
+  }
+
+  unset($old);
+  unset($new);
+  return $disallowed_changes;
+}
+
+/**
+ * Helper function to check for term reference changes disallowed by create.
+ *
+ * @param array $new_field
+ *   The entity or form values of the updated field.
+ * @param array $old_field
+ *   The entity or form values of the original field.
+ *
+ * @return array|false
+ *   Returns FALSE if there are no disallowed changes.  Otherwise, an array:
+ *   - 'added' => An array of added terms that are disallowed.
+ *   - 'removed' => An array of removed termss that are disallowed.
+ */
+function _taxonomy_access_disallowed_changes(array $new_field, array $old_field) {
+
+  // Assemble a list of term IDs from the original entity, if any.
+  $old_tids = array();
+  foreach ($old_field as $old_item) {
+    // Some things are NULL for some reason.
+    if ($old_item['tid']) {
+      $old_tids[] = $old_item['tid'];
+    }
+  }
+
+  // Assemble a list of term IDs from the updated entity.
+  $new_tids = array();
+  foreach ($new_field as $delta => $new_item) {
+    // Some things are NULL for some reason.
+    // Allow the special tid "autocreate" so users can create new terms.
+    if ($new_item['tid'] && ($new_item['tid'] != 'autocreate')) {
+      $new_tids[$delta] = $new_item['tid'];
+    }
+  }
+
+  // Check for added tids, and unset ones the user may not add.
+  $added = array_diff($new_tids, $old_tids);
+  $may_not_add = taxonomy_access_create_disallowed($added);
+
+  // Check for removed tids, and restore ones the user may not remove.
+  $removed = array_diff($old_tids, $new_tids);
+  $may_not_remove = taxonomy_access_create_disallowed($removed);
+
+  // If there were any disallowed changes, return them.
+  if (!empty($may_not_add) || !empty($may_not_remove)) {
+    return array('added' => $may_not_add, 'removed' => $may_not_remove);
+  }
+
+  // Return FALSE if all changes were valid.
+  return FALSE;
+}
+
+/**
+ * End of "defgroup tac_create".
+ * @}
+ */

+ 94 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.css

@@ -0,0 +1,94 @@
+table.grant_help th {
+ vertical-align: top;
+}
+
+table.grant_help td p {
+ margin-top: 0px;
+ margin-bottom: 8px;
+}
+
+table.grant_help em.perm {
+  font-weight: bold;
+}
+
+label.disabled {
+  color: #999;
+  font-style: italic;
+}
+.view-0,
+.update-0,
+.delete-0 {
+  background-color: #fffce5;
+}
+.view-1,
+.update-1,
+.delete-1,
+.create-1,
+.list-1 {
+  background-color: #e5ffe2;
+}
+.view-2,
+.update-2,
+.delete-2,
+.create-0,
+.list-0 {
+ background-color: #fef5f1;
+}
+.taxonomy-access-grant-table {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+  width: inherit;
+}
+.taxonomy-access-grant-table th.select-all {
+  width: 2em;
+}
+.taxonomy-access-grant-table td.taxonomy-access-label {
+  padding-right: 4em;
+  min-width: 12em;
+}
+.taxonomy-access-grant-table td.taxonomy-access-default {
+  font-style: italic;
+  font-weight: bold;
+}
+.taxonomy-access-grant-table td.taxonomy-access-grant {
+  width: 7em;
+}
+fieldset#edit-vocabs .fieldset-description {
+  display: block;
+}
+
+/**
+ * I apologize, because what follows is a hodgepodge to override the default
+ * fieldset styling in both Seven and Bartik, and might not even work in some
+ * themes.
+ */
+#taxonomy-access-admin-role fieldset {
+  margin-bottom: 0.5em;
+  margin-top: 0.5em;
+}
+
+#taxonomy-access-admin-role fieldset.taxonomy-access-add.collapsible,
+#taxonomy-access-admin-role fieldset.taxonomy-access-add.collapsed {
+  margin-top: 0px;
+  margin-bottom: 1em;
+  border: none;
+  background: none;
+}
+fieldset.taxonomy-access-add > legend {
+  background: none;
+  border: none;
+  font-family: inherit;
+  position: static;
+}
+fieldset.taxonomy-access-add > legend a {
+  color: #0076bc;
+}
+fieldset.taxonomy-access-add  .fieldset-wrapper {
+  margin-top: 0px;
+  padding-top: 0.5em;
+}
+html.js fieldset.collapsible.taxonomy-access-add > legend .fieldset-legend,
+html.js fieldset.collapsed.taxonomy-access-add > legend .fieldset-legend {
+  background: transparent url(images/add.png) no-repeat 0 center;
+  text-transform: none;
+}

+ 14 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.info

@@ -0,0 +1,14 @@
+name = Taxonomy Access Control
+description = Access control for user roles based on taxonomy categories.
+dependencies[] = taxonomy (>=7.8)
+core = 7.x
+configure = admin/config/people/taxonomy_access
+
+files[] = taxonomy_access.test
+
+; Information added by Drupal.org packaging script on 2015-09-19
+version = "7.x-1.0"
+core = "7.x"
+project = "taxonomy_access"
+datestamp = "1442635740"
+

+ 302 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.install

@@ -0,0 +1,302 @@
+<?php
+/**
+ * @file
+ *  Install, update, and uninstall functions for Taxonomy Access Control.
+ */
+
+
+/**
+ * Implements hook_update_last_removed().
+ */
+function taxonomy_access_last_removed() {
+  return 5;
+}
+
+
+/**
+ * Implements hook_install().
+ *
+ * Adds tables to database: 'taxonomy_access_term', 'taxonomy_access_default'
+ */
+function taxonomy_access_install() {
+
+  // Default global perms for roles 1 (anonymous) and 2 (authenticated).
+  db_query(
+    'INSERT INTO {taxonomy_access_default}
+    (vid, rid, grant_view, grant_update, grant_delete, grant_create, grant_list)
+    VALUES
+    (:vid, :rid, :node_allow, :ignore, :ignore, :term_allow, :term_allow)',
+    array(
+      ':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT,
+      ':rid' => DRUPAL_ANONYMOUS_RID,
+      ':node_allow' => TAXONOMY_ACCESS_NODE_ALLOW,
+      ':ignore' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ':term_allow' => TAXONOMY_ACCESS_TERM_ALLOW)
+  );
+  db_query(
+    'INSERT INTO {taxonomy_access_default}
+    (vid, rid, grant_view, grant_update, grant_delete, grant_create, grant_list)
+    VALUES
+    (:vid, :rid, :node_allow, :ignore, :ignore, :term_allow, :term_allow)',
+    array(
+      ':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT,
+      ':rid' => DRUPAL_AUTHENTICATED_RID,
+      ':node_allow' => TAXONOMY_ACCESS_NODE_ALLOW,
+      ':ignore' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ':term_allow' => TAXONOMY_ACCESS_TERM_ALLOW)
+  );
+}
+
+/**
+ * Implements hook_schema().
+ */
+function taxonomy_access_schema() {
+  $schema = array();
+
+  $schema['taxonomy_access_term'] = array(
+    'description' => 'Identifies which roles may view, update, delete, create, and list nodes with a given term.',
+    'fields' => array(
+      'tid' => array(
+        'description' => 'The term_data.tid this record affects.  Overrides vocabulary default in taxonomy_access_default.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_VOCABULARY_DEFAULT,
+      ),
+      'rid' => array(
+        'description' => "The role.rid a user must possess to gain this row's privileges on nodes for this term.",
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_view' => array(
+        'description' => 'Whether this role can view nodes with this term. 0=>Ignore, 1=>Allow, 2=>Deny.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ),
+      'grant_update' => array(
+        'description' => 'Whether this role can edit nodes with this term. 0=>Ignore, 1=>Allow, 2=>Deny.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ),
+      'grant_delete' => array(
+        'description' => 'Whether this role can delete nodes with this term. 0=>Ignore, 1=>Allow, 2=>Deny.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ),
+      'grant_create' => array(
+        'description' => 'Whether this role can set this term when adding or editing a node. 0=>No, 1=>Yes.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_TERM_DENY,
+      ),
+      'grant_list' => array(
+        'description' => 'Whether this role can view the name of this term on a node or in category lists. 0=>No, 1=>Yes.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_TERM_ALLOW,
+      ),
+    ),
+    'primary key' => array('tid', 'rid'),
+  );
+
+  $schema['taxonomy_access_default'] = array(
+    'description' => 'Sets vocabulary defaults for which roles may view, update, delete, create, and list nodes with a given term. Overridden by {taxonomy_access_term}.',
+    'fields' => array(
+      'vid' => array(
+        'description' => 'The vocabulary.vid for which this row sets defaults.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_VOCABULARY_DEFAULT,
+      ),
+      'rid' => array(
+        'description' => "The role.rid a user must possess to gain this row's privileges on nodes for terms in this vocabulary.",
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'grant_view' => array(
+        'description' => 'Whether this role can view nodes with terms in this vocabulary. 0=>Ignore, 1=>Allow, 2=>Deny.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ),
+      'grant_update' => array(
+        'description' => 'Whether this role can edit nodes with terms in this vocabulary. 0=>Ignore, 1=>Allow, 2=>Deny.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ),
+      'grant_delete' => array(
+        'description' => 'Whether this role can delete nodes with terms in this vocabulary. 0=>Ignore, 1=>Allow, 2=>Deny.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_NODE_IGNORE,
+      ),
+      'grant_create' => array(
+        'description' => 'Whether this role can set terms in this vocabulary when adding or editing a node. 0=>No, 1=>Yes.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_TERM_DENY,
+      ),
+      'grant_list' => array(
+        'description' => 'Whether this role can view the name of terms in this vocabulary on a node or in category lists. 0=>No, 1=>Yes.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => TAXONOMY_ACCESS_TERM_DENY,
+      ),
+    ),
+    'primary key' => array('vid', 'rid'),
+  );
+
+  return $schema;
+}
+
+/**
+ * Add vocabulary defaults for all configured vocabularies.
+ */
+function taxonomy_access_update_7002() {
+  // Get a list of all vocabularies with any term configurations for each role.
+  $ta_configs = db_query(
+    "SELECT td.vid, ta.rid
+     FROM {taxonomy_access_term} ta
+     INNER JOIN {taxonomy_term_data} td ON ta.tid = td.tid
+     GROUP BY td.vid, ta.rid"
+  )->fetchAll();
+
+  // Get a list of all configured vocabularies.
+  $td_configs = db_query(
+    "SELECT vid, rid
+     FROM {taxonomy_access_default}"
+  )->fetchAll();
+
+  $records = array();
+  $global_defaults = taxonomy_access_global_defaults();
+
+  foreach ($ta_configs as $config) {
+    if (!in_array($config, $td_configs)) {
+      $record = (array) $global_defaults[$config->rid];
+      $records[] = _taxonomy_access_format_grant_record($config->vid, $config->rid, $record, TRUE);
+    }
+  }
+
+  if (taxonomy_access_set_default_grants($records)) {
+    return t('Update completed successfully.');
+  }
+  else {
+    return t('Update failed.');
+  }
+}
+
+/**
+ * Rename grant realm.
+ */
+function taxonomy_access_update_7001() {
+  db_query(
+    "UPDATE {node_access} SET realm = 'taxonomy_access_role'
+    WHERE realm = 'term_access'"
+  );
+}
+
+/**
+ * Rename database tables to follow Drupal 7 standards.
+ */
+function taxonomy_access_update_7000() {
+  db_rename_table('term_access', 'taxonomy_access_term');
+  db_rename_table('term_access_defaults', 'taxonomy_access_default');
+}
+
+/**
+ * Implements hook_enable().
+ *
+ * Housekeeping: while we were away, did you delete any terms/vocabs/roles?
+ * 1: Weight this module below the Taxonomy module.
+ * 2: Delete ta, tad rows for missing roles.
+ * 3: Delete ta rows for missing terms.
+ * 4: Delete tad rows for missing vocabs.
+ */
+function taxonomy_access_enable() {
+
+  // Weight this module below the Taxonomy module.
+  $tax_weight =
+    db_query(
+      "SELECT weight FROM {system}
+      WHERE name = 'taxonomy'"
+    )
+    ->fetchField()
+    ;
+
+  db_update('system')
+  ->fields(array('weight' => ($tax_weight + 1)))
+  ->condition('name', 'taxonomy_access')
+  ->execute();
+
+  // Delete any records for roles not in {roles}.
+  $roles = _taxonomy_access_user_roles();
+  $config_roles =
+    db_query("SELECT DISTINCT rid FROM {taxonomy_access_default}")
+    ->fetchCol();
+  $missing_roles = array_diff($config_roles, array_keys($roles));
+
+  // Core flags node access for rebuild on enable, so skip node updates.
+  foreach ($missing_roles as $rid) {
+    taxonomy_access_delete_role_grants($rid, FALSE);
+  }
+
+  // Delete any term configurations not in {taxonomy_term_data}.
+  $term_ids =
+    db_query(
+      "SELECT ta.tid
+      FROM {taxonomy_access_term} ta
+      LEFT JOIN {taxonomy_term_data} td ON ta.tid = td.tid
+      WHERE ta.tid <> :tid AND td.tid IS NULL",
+      array(':tid' => TAXONOMY_ACCESS_VOCABULARY_DEFAULT))
+    ->fetchCol()
+    ;
+
+  // Core flags node access for rebuild on enable, so skip node updates.
+  taxonomy_access_delete_term_grants($term_ids, NULL, FALSE);
+  unset($term_ids);
+
+  // Delete any defaults for vocabularies not in {taxonomy_vocabulary}.
+  $vocab_ids =
+    db_query(
+      "SELECT tad.vid
+      FROM {taxonomy_access_default} tad
+      LEFT JOIN {taxonomy_vocabulary} tv ON tad.vid = tv.vid
+      WHERE tad.vid <> :vid AND tv.vid IS NULL",
+      array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT))
+    ->fetchCol()
+    ;
+
+  // Core flags node access for rebuild on enable, so skip node updates.
+  taxonomy_access_delete_default_grants($vocab_ids, FALSE);
+  unset($vocab_ids);
+
+}

+ 1775 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.module

@@ -0,0 +1,1775 @@
+<?php
+
+/**
+ * @file
+ * Allows administrators to specify access control for taxonomy categories.
+ */
+
+/**
+ * Maximum number of nodes for which to update node access within the module.
+ *
+ * If the number of affected nodes is greater, then node_access_needs_rebuild()
+ * will be set instead.
+ */
+define('TAXONOMY_ACCESS_MAX_UPDATE', 500);
+
+/**
+ * Base path for module administration pages.
+ */
+define('TAXONOMY_ACCESS_CONFIG', 'admin/config/people/taxonomy_access');
+
+/**
+ * Global default.
+ */
+define('TAXONOMY_ACCESS_GLOBAL_DEFAULT', 0);
+
+/**
+ * Vocabulary default.
+ */
+define('TAXONOMY_ACCESS_VOCABULARY_DEFAULT', 0);
+
+/**
+ * 'Allow' grant value for nodes.
+ */
+define('TAXONOMY_ACCESS_NODE_ALLOW', 1);
+
+/**
+ * 'Ignore' grant value for nodes.
+ */
+define('TAXONOMY_ACCESS_NODE_IGNORE', 0);
+
+/**
+ * 'Deny' grant value for nodes.
+ */
+define('TAXONOMY_ACCESS_NODE_DENY', 2);
+
+/**
+ * 'Allow' grant value for terms.
+ */
+define('TAXONOMY_ACCESS_TERM_ALLOW', 1);
+
+/**
+ * 'Deny' grant value for terms.
+ */
+define('TAXONOMY_ACCESS_TERM_DENY', 0);
+
+/**
+ * Caches a list of all roles.
+ *
+ * @param string|null $permission
+ *   (optional) A string containing a permission.  If set, only roles
+ *   containing that permission are returned.  Defaults to NULL.
+ *
+ * @return array
+ *   An array of roles from user_roles().
+ *
+ * @todo
+ *   Replace this function once http://drupal.org/node/6463 is backported.
+ */
+function _taxonomy_access_user_roles($permission = NULL) {
+  $roles = &drupal_static(__FUNCTION__, array());
+  if (!isset($roles[$permission])) {
+    $roles[$permission] = user_roles(FALSE, $permission);
+  }
+  return $roles[$permission];
+}
+
+/**
+ * Implements hook_init().
+ */
+function taxonomy_access_init() {
+  $path = drupal_get_path('module', 'taxonomy_access');
+  drupal_add_css($path . '/taxonomy_access.css');
+
+  // Register our shutdown function.
+  drupal_register_shutdown_function('taxonomy_access_shutdown');
+}
+
+/**
+ * Implements hook_theme().
+ */
+function taxonomy_access_theme() {
+  return array(
+    'taxonomy_access_admin_form' => array(
+      'render element' => 'form',
+      'file' => 'taxonomy_access.admin.inc',
+    ),
+    'taxonomy_access_grant_table' => array(
+      'render element' => 'elements',
+      'file' => 'taxonomy_access.admin.inc',
+    ),
+  );
+}
+
+/**
+ * Implements hook_element_info().
+ */
+function taxonomy_access_element_info() {
+  return array(
+    'taxonomy_access_grant_table' => array(
+      '#theme' => 'taxonomy_access_grant_table',
+      '#regions' => array('' => array()),
+    ),
+  );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function taxonomy_access_menu() {
+  $items = array();
+
+  $items[TAXONOMY_ACCESS_CONFIG] = array(
+    'title' => 'Taxonomy access control',
+    'description' => 'Taxonomy-based access control for content',
+    'page callback' => 'taxonomy_access_admin',
+    'access arguments' => array('administer permissions'),
+    'file' => 'taxonomy_access.admin.inc',
+  );
+  $items[TAXONOMY_ACCESS_CONFIG . '/role'] = array(
+    'title' => 'Configure role access rules',
+    'description' => 'Configure taxonomy access control',
+    'page callback' => 'taxonomy_access_admin',
+    'access arguments' => array('administer permissions'),
+    'file' => 'taxonomy_access.admin.inc',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items[TAXONOMY_ACCESS_CONFIG . '/role/%/edit'] = array(
+    'title callback' => 'taxonomy_access_role_edit_title',
+    'title arguments' => array(5),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('taxonomy_access_admin_role', 5),
+    'access callback' => 'taxonomy_access_role_edit_access',
+    'access arguments' => array(5),
+    'file' => 'taxonomy_access.admin.inc',
+  );
+  $items[TAXONOMY_ACCESS_CONFIG . '/role/%/enable'] = array(
+    'page callback' => 'taxonomy_access_enable_role_validate',
+    'page arguments' => array(5),
+    'access arguments' => array('administer permissions'),
+    'file' => 'taxonomy_access.admin.inc',
+  );
+  $items[TAXONOMY_ACCESS_CONFIG . '/role/%/delete'] = array(
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('taxonomy_access_role_delete_confirm', 5),
+    'access callback' => 'taxonomy_access_role_delete_access',
+    'access arguments' => array(5),
+    'file' => 'taxonomy_access.admin.inc',
+    'type' => MENU_CALLBACK,
+  );
+  $items[TAXONOMY_ACCESS_CONFIG . '/role/%/disable/%taxonomy_vocabulary'] = array(
+    'page callback' => 'taxonomy_access_disable_vocab_confirm_page',
+    'page arguments' => array(5, 7),
+    'access arguments' => array('administer permissions'),
+    'file' => 'taxonomy_access.admin.inc',
+    'type' => MENU_CALLBACK,
+  );
+  $items['taxonomy_access/autocomplete'] = array(
+    'title' => 'Autocomplete taxonomy',
+    'page callback' => 'taxonomy_access_autocomplete',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+    'file' => 'taxonomy_access.create.inc',
+  );
+
+  return $items;
+}
+
+/**
+ * Title callback: Returns the title for the role edit form.
+ */
+function taxonomy_access_role_edit_title($rid) {
+  $roles = _taxonomy_access_user_roles();
+  return t('Access rules for @role', array('@role' => $roles[$rid]));
+}
+
+/**
+ * Access callback: Determines whether the admin form can be accessed.
+ */
+function taxonomy_access_role_edit_access($rid) {
+  // Allow access only if the user may administer permissions.
+  if (!user_access('administer permissions')) {
+    return FALSE;
+  }
+
+  // Do not render the form for invalid role IDs.
+  $roles = _taxonomy_access_user_roles();
+  if (empty($roles[$rid])) {
+    return FALSE;
+  }
+
+  // If the conditions above are met, grant access.
+  return TRUE;
+}
+
+
+/**
+ * Access callback for role deletion form.
+ */
+function taxonomy_access_role_delete_access($rid) {
+  if (!user_access('administer permissions')) {
+    return FALSE;
+  }
+  if (($rid == DRUPAL_ANONYMOUS_RID) || ($rid == DRUPAL_AUTHENTICATED_RID)) {
+    return FALSE;
+  }
+
+  $roles = _taxonomy_access_user_roles();
+  if (empty($roles[$rid])) {
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+/**
+ * Implements hook_user_role_delete().
+ */
+function taxonomy_access_user_role_delete($role) {
+  // Do not update node access since the role will no longer exist.
+  taxonomy_access_delete_role_grants($role->rid, FALSE);
+}
+
+/**
+ * Implements hook_taxonomy_vocabulary_delete().
+ */
+function taxonomy_access_taxonomy_vocabulary_delete($vocab) {
+  taxonomy_access_delete_default_grants($vocab->vid);
+}
+
+/**
+ * Implements hook_taxonomy_term_delete().
+ */
+function taxonomy_access_taxonomy_term_delete($term) {
+  taxonomy_access_delete_term_grants($term->tid);
+}
+
+/**
+ * Implements hook_node_grants().
+ *
+ * Gives access to taxonomies based on the taxonomy_access table.
+ */
+function taxonomy_access_node_grants($user, $op) {
+  $roles = is_array($user->roles) ? array_keys($user->roles) : -1;
+  return array('taxonomy_access_role' => $roles);
+}
+
+/**
+ * Implements hook_node_access_records().
+ *
+ * @ingroup tac_node_access
+ */
+function taxonomy_access_node_access_records($node) {
+  // Only write grants for published nodes.
+  if ($node->status) {
+    // Make sure to reset caches for accurate grant values.
+    return _taxonomy_access_node_access_records($node->nid, TRUE);
+  }
+}
+
+/**
+ * Implements hook_field_info_alter().
+ *
+ * @todo
+ *   Should we somehow pass the originl callback to our callback dynamically?
+ */
+function taxonomy_access_field_info_alter(&$info) {
+
+  // Return if there's no term reference field type.
+  if (empty($info['taxonomy_term_reference'])) {
+    return;
+  }
+
+  // Use our custom callback in order to disable list while generating options.
+  $info['taxonomy_term_reference']['settings']['options_list_callback'] = '_taxonomy_access_term_options';
+}
+
+/**
+ * Implements hook_field_attach_validate().
+ *
+ * For form validation:
+ *   @see taxonomy_access_options_validate()
+ *   @see taxonomy_access_autocomplete_validate()
+ */
+function taxonomy_access_field_attach_validate($entity_type, $entity, &$errors) {
+  // Add create grant handling.
+  module_load_include('inc', 'taxonomy_access', 'taxonomy_access.create');
+
+  _taxonomy_access_field_validate($entity_type, $entity, $errors);
+}
+
+/**
+ * Implements hook_query_TAG_alter() for 'term_access'.
+ *
+ * Provides sitewide list grant filtering, as well as create grant filtering
+ * for autocomplete paths.
+ *
+ * @todo
+ *   Fix create permission filtering for autocomplete paths.
+ *
+ * @ingroup tac_list
+ */
+function taxonomy_access_query_term_access_alter($query) {
+
+  // Take no action while the list op is disabled.
+  if (!taxonomy_access_list_enabled()) {
+    return;
+  }
+
+  // Take no action if there is no term table in the query.
+  $alias = '';
+  $tables =& $query->getTables();
+  foreach ($tables as $i => $table) {
+    if (strpos($table['table'], 'taxonomy_term_') === 0) {
+      $alias = $table['alias'];
+    }
+  }
+  if (empty($alias)) {
+    return;
+  }
+
+  // Fetch a list of all terms the user may list.
+  $tids = &drupal_static(__FUNCTION__, taxonomy_access_user_list_terms());
+
+  // If exactly TRUE was returned, the user can list all terms.
+  if ($tids === TRUE) {
+    return;
+  }
+
+  // If the user cannot list any terms, then allow only null values.
+  if (empty($tids)) {
+    $query->isNull($alias . ".tid");
+  }
+
+  // Otherwise, filter to the terms provided.
+  else {
+    $query->condition($alias . ".tid", $tids, "IN");
+  }
+}
+
+/**
+ * Implements hook_field_widget_WIDGET_TYPE_form_alter().
+ *
+ * @see _taxonomy_access_autocomplete_alter()
+ */
+function taxonomy_access_field_widget_taxonomy_autocomplete_form_alter(&$element, &$form_state, $context) {
+
+  // Enforce that list grants do not filter the autocomplete.
+  taxonomy_access_disable_list();
+
+  // Add create grant handling.
+  module_load_include('inc', 'taxonomy_access', 'taxonomy_access.create');
+  _taxonomy_access_autocomplete_alter($element, $form_state, $context);
+
+  // Re-enable list grants.
+  taxonomy_access_enable_list();
+}
+
+/**
+ * Implements hook_field_widget_form_alter().
+ *
+ * @see _taxonomy_access_options_alter()
+ */
+function taxonomy_access_field_widget_form_alter(&$element, &$form_state, $context) {
+  // Only act on taxonomy fields.
+  if ($context['field']['type'] != 'taxonomy_term_reference') {
+    return;
+  }
+  // Only act on options widgets.
+  $widget = $context['instance']['widget']['type'];
+  if (!in_array($widget, array('options_buttons', 'options_select'))) {
+    return;
+  }
+
+  // Enforce that list grants do not filter our queries.
+  taxonomy_access_disable_list();
+
+  // Add create grant handling.
+  module_load_include('inc', 'taxonomy_access', 'taxonomy_access.create');
+  _taxonomy_access_options_alter($element, $form_state, $context);
+
+  // Re-enable list grants.
+  taxonomy_access_enable_list();
+}
+
+/**
+ * Enables access control for a given role.
+ *
+ * @param int $rid
+ *   The role ID.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ *
+ * @todo
+ *   Should we default to the authenticated user global default?
+ */
+function taxonomy_access_enable_role($rid) {
+  $rid = intval($rid);
+
+  // Take no action if the role is already enabled. All valid role IDs are > 0.
+  if (!$rid || taxonomy_access_role_enabled($rid)) {
+    return FALSE;
+  }
+
+  // If we are adding a role, no global default is set yet, so insert it now.
+  // Assemble a $row object for Schema API.
+  $row = new stdClass();
+  $row->vid = TAXONOMY_ACCESS_GLOBAL_DEFAULT;
+  $row->rid = $rid;
+
+  // Insert the row with defaults for all grants.
+  return drupal_write_record('taxonomy_access_default', $row);
+}
+
+/**
+ * Indicates whether access control is enabled for a given role.
+ *
+ * @param int $rid
+ *   The role ID.
+ *
+ * @return bool
+ *   TRUE if access control is enabled for the role, or FALSE otherwise.
+ */
+function taxonomy_access_role_enabled($rid) {
+  $role_status = &drupal_static(__FUNCTION__, array());
+  if (!isset($role_status[$rid])) {
+    $role_status[$rid] =
+      db_query(
+        'SELECT 1
+         FROM {taxonomy_access_default}
+         WHERE rid = :rid AND vid = :vid',
+        array(':rid' => $rid, ':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT))
+      ->fetchField();
+  }
+  return (bool) $role_status[$rid];
+}
+
+/**
+ * Enables a vocabulary for the given role.
+ *
+ * @param int $vid
+ *   The vocabulary ID to enable.
+ * @param int $rid
+ *   The role ID.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ *
+ * @see taxnomomy_access_enable_role()
+ */
+function taxonomy_access_enable_vocab($vid, $rid) {
+  $rid = intval($rid);
+  $vid = intval($vid);
+
+  // All valid role IDs are > 0, and we do not enable the global default here.
+  if (!$rid || !$vid) {
+    return FALSE;
+  }
+  // Take no action if the vocabulary is already enabled for the role.
+  $vocab_status =
+    db_query(
+      'SELECT 1
+       FROM {taxonomy_access_default}
+       WHERE rid = :rid AND vid = :vid',
+      array(':rid' => $rid, ':vid' => $vid))
+    ->fetchField();
+  if ($vocab_status) {
+    return FALSE;
+  }
+  // Otherwise, initialize the vocabulary default with the global default.
+  // Use our API functions so that node access gets updated as needed.
+  $global_default =
+    db_query(
+      'SELECT grant_view, grant_update, grant_delete, grant_create, grant_list
+       FROM {taxonomy_access_default}
+       WHERE vid = :vid AND rid = :rid',
+       array(':rid' => $rid, ':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT))
+    ->fetchAssoc();
+  $record = _taxonomy_access_format_grant_record($vid, $rid, $global_default, TRUE);
+  return taxonomy_access_set_default_grants(array($vid => $record));
+}
+
+/**
+ * Disables a vocabulary for the given role.
+ *
+ * @param int $vid
+ *   The vocabulary ID to enable.
+ * @param int $rid
+ *   The role ID.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ *
+ * @see taxonomy_access_delete_role_grants()
+ */
+function taxonomy_access_disable_vocab($vid, $rid) {
+  $rid = intval($rid);
+  $vid = intval($vid);
+
+  // Do not allow the global default to be deleted this way.
+  // Deleting the global default would disable the role.
+  if (!$vid || !$rid) {
+    return FALSE;
+  }
+
+  // Delete the vocabulary default.
+  taxonomy_access_delete_default_grants($vid, $rid);
+
+  // Delete the role's term access rules for the vocabulary.
+  // First check which term records are enabled so we can update node access.
+  $tids =
+    db_query(
+      "SELECT ta.tid
+       FROM {taxonomy_access_term} ta
+       INNER JOIN {taxonomy_term_data} td ON ta.tid = td.tid
+       WHERE td.vid = :vid AND ta.rid = :rid",
+      array(':vid' => $vid, ':rid' => $rid))
+    ->fetchCol();
+  taxonomy_access_delete_term_grants($tids, $rid);
+
+  return TRUE;
+}
+
+
+/**
+ * @defgroup tac_affected_nodes Taxonomy Access Control: Node access update mechanism
+ * @{
+ * Update node access on shutdown in response to other changes.
+ */
+
+
+/**
+ * Shutdown function: Performs any needed node access updates.
+ *
+ * @see taxonomy_access_init()
+ */
+function taxonomy_access_shutdown() {
+  // Update any affected nodes.
+  $affected_nodes = taxonomy_access_affected_nodes();
+  if (!empty($affected_nodes)) {
+    taxonomy_access_affected_nodes(NULL, TRUE);
+    _taxonomy_access_node_access_update($affected_nodes);
+  }
+}
+
+/**
+ * Flags node access for rebuild with a message for administrators.
+ */
+function _taxonomy_access_flag_rebuild() {
+  drupal_set_message(t("Taxonomy Access Control is updating node access... If you see a message that content access permissions need to be rebuilt, you may wait until after you have completed your configuration changes."), 'status');
+  node_access_needs_rebuild(TRUE);
+}
+
+
+/**
+ * Updates node access grants for a set of nodes.
+ *
+ * @param array $nids
+ *   An array of node IDs for which to acquire access permissions.
+ *
+ * @todo
+ *   Unset rebuild message when we set the flag to false?
+ */
+function _taxonomy_access_node_access_update(array $nids) {
+  // Proceed only if node_access_needs_rebuild() is not already flagged.
+  if (!node_access_needs_rebuild()) {
+
+    // Set node_access_needs_rebuild() until we succeed below.
+    _taxonomy_access_flag_rebuild();
+
+    // Remove any duplicate nids from the array.
+    $nids = array_unique($nids);
+
+    // If the number of nodes is small enough, update node access for each.
+    if (sizeof($nids) < TAXONOMY_ACCESS_MAX_UPDATE) {
+      foreach ($nids as $node) {
+        $loaded_node = node_load($node, NULL, TRUE);
+        if (!empty($loaded_node)) {
+          node_access_acquire_grants($loaded_node);
+        }
+      }
+
+      // If we make it here our update was successful; unflag rebuild.
+      node_access_needs_rebuild(FALSE);
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Caches and retrieves nodes affected by a taxonomy change.
+ *
+ * @param array $affected_nodes
+ *   (optional) If we are caching, the list of nids to cache.
+ *   Defaults to NULL.
+ * @param bool $reset
+ *   (optional) Flag to manually reset the list.  Defaults to FALSE.
+ *
+ * @return
+ *   The cached list of nodes.
+ */
+function taxonomy_access_affected_nodes(array $affected_nodes = NULL, $reset = FALSE) {
+  static $nodes = array();
+
+  // If node_access_needs_rebuild or $reset are set, reset list and return.
+  if (!empty($nodes)) {
+    if (node_access_needs_rebuild() || $reset) {
+      $nodes = array();
+      return;
+    }
+  }
+
+  // If we were passed a list of nodes, cache.
+  if (isset($affected_nodes)) {
+    $nodes = array_unique(array_merge($nodes, $affected_nodes));
+
+    // Stop caching if there are more nodes than the limit.
+    if (sizeof($nodes) >= TAXONOMY_ACCESS_MAX_UPDATE) {
+      _taxonomy_access_flag_rebuild();
+      unset($nodes);
+    }
+  }
+
+  // Otherwise, return the cached data.
+  else {
+    return $nodes;
+  }
+}
+
+/**
+ * Gets node IDs with controlled terms or vocabs for any of the given roles.
+ *
+ * @param int $rid
+ *    A single role ID.
+ *
+ * @return array
+ *    An array of node IDs associated with terms or vocabularies that are
+ *    controlled for the role.
+ */
+function _taxonomy_access_get_controlled_nodes_for_role($rid) {
+  $query = db_select('taxonomy_index', 'ti')
+    ->fields('ti', array('nid'))
+    ->addTag('taxonomy_access_node');
+  $query->leftJoin('taxonomy_term_data', 'td', 'ti.tid = td.tid');
+  $query->leftJoin('taxonomy_access_term', 'ta', 'ti.tid = ta.tid');
+  $query->leftJoin('taxonomy_access_default', 'tad', 'tad.vid = td.vid');
+
+  // The query builder will automatically use = or IN() as appropriate.
+  $query->condition(
+    db_or()
+    ->condition('ta.rid', $rid)
+    ->condition('tad.rid', $rid)
+  );
+
+  $nids = $query->execute()->fetchCol();
+  return $nids;
+}
+
+/**
+ * Gets node IDs associated with the roles' global defaults.
+ *
+ * @param int $rid
+ *   A single role ID.
+ *
+ * @return array
+ *    An array of node IDs associated with the global default.
+ */
+function _taxonomy_access_get_nodes_for_global_default($rid) {
+  // Two kinds of nodes are governed by the global default:
+  // 1. Nodes with terms controlled neither directly nor by vocab. defaults,
+  // 2. Nodes with no terms.
+
+  // Get a list of all terms controlled for the role, either directly or
+  // by a vocabulary default.
+  $tids = _taxonomy_access_global_controlled_terms($rid);
+
+  $query =
+    db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addTag('taxonomy_access_node')
+    ;
+
+  // With a left join, the term ID for untagged nodes will be NULL.
+  if (!empty($tids)) {
+    $query->leftJoin('taxonomy_index', 'ti', 'ti.nid = n.nid');
+    $query->condition(
+      db_or()
+      ->condition('ti.tid', $tids, 'NOT IN')
+      ->isNull('ti.tid')
+    );
+  }
+
+  $nids = $query->execute()->fetchCol();
+
+  return $nids;
+}
+
+/**
+ * Gets node IDs associated with a given vocabulary.
+ *
+ * @param int|array $vocab_ids
+ *    A single vocabulary ID or an array of IDs.
+ * @param int $rid.
+ *    (optional) A single role ID.
+ *    This argument has the effect of filtering out nodes in terms that
+ *    are already controlled invidually for the role.  Defaults to NULL.
+ *
+ * @return array
+ *    An array of node IDs associated with the given vocabulary.
+ */
+function _taxonomy_access_get_nodes_for_defaults($vocab_ids, $rid = NULL) {
+  // Accept either a single vocabulary ID or an array thereof.
+  if (is_numeric($vocab_ids)) {
+    $vocab_ids = array($vocab_ids);
+  }
+  if (empty($vocab_ids)) {
+    return FALSE;
+  }
+
+  // If a role was passed, get terms controlled for that role.
+  if (!empty($rid)) {
+    $tids = _taxonomy_access_vocab_controlled_terms($vocab_ids, $rid);
+  }
+
+  $query =
+    db_select('taxonomy_index', 'ti')
+    ->condition('td.vid', $vocab_ids)
+    ->fields('ti', array('nid'))
+    ->addTag('taxonomy_access_node');
+    ;
+  $query->join('taxonomy_term_data', 'td', 'td.tid = ti.tid');
+
+  // Exclude records with controlled terms from the results.
+  if (!empty($tids)) {
+    $query->condition('ti.tid', $tids, 'NOT IN');
+  }
+
+  $nids = $query->execute()->fetchCol();
+  unset($tids);
+  unset($query);
+
+  // If the global default is in the list, fetch those nodes as well.
+  if (in_array(TAXONOMY_ACCESS_GLOBAL_DEFAULT, $vocab_ids)) {
+    $nids =
+      array_merge($nids, _taxonomy_access_get_nodes_for_global_default($rid));
+  }
+
+  return $nids;
+}
+
+/**
+ * Retrieves a list of terms controlled by the global default for a role.
+ *
+ * @param int $rid
+ *   The role ID.
+ *
+ * @return array
+ *   A list of term IDs.
+ */
+function _taxonomy_access_global_controlled_terms($rid) {
+  $tids =
+    db_query(
+      "SELECT td.tid
+       FROM {taxonomy_term_data} td
+       LEFT JOIN {taxonomy_access_term} ta ON td.tid = ta.tid
+       LEFT JOIN {taxonomy_access_default} tad ON td.vid = tad.vid
+       WHERE ta.rid = :rid OR tad.rid = :rid",
+      array(':rid' => $rid)
+    )
+    ->fetchCol();
+
+  return $tids;
+}
+
+/**
+ * Retrieves a list of terms controlled by the global default for a role.
+ *
+ * @param int $rid
+ *   The role ID.
+ *
+ * @return array
+ *   A list of term IDs.
+ */
+function _taxonomy_access_vocab_controlled_terms($vids, $rid) {
+  // Accept either a single vocabulary ID or an array thereof.
+  if (is_numeric($vids)) {
+    $vids = array($vids);
+  }
+
+  $tids =
+    db_query(
+      "SELECT td.tid
+       FROM {taxonomy_term_data} td
+       INNER JOIN {taxonomy_access_term} ta ON td.tid = ta.tid
+       WHERE ta.rid = :rid
+       AND td.vid IN (:vids)",
+      array(':rid' => $rid, ':vids' => $vids)
+    )
+    ->fetchCol();
+
+  return $tids;
+}
+
+/**
+ * Gets node IDs associated with a given term.
+ *
+ * @param int|array $term_ids
+ *   A single term ID or an array of term IDs.
+ *
+ * @return array
+ *    An array of node IDs associated with the given terms.
+ */
+function _taxonomy_access_get_nodes_for_terms($term_ids) {
+  if (empty($term_ids)) {
+    return FALSE;
+  }
+
+  // The query builder will use = or IN() automatically as appropriate.
+  $nids =
+    db_select('taxonomy_index', 'ti')
+    ->condition('ti.tid', $term_ids)
+    ->fields('ti', array('nid'))
+    ->addTag('taxonomy_access_node')
+    ->execute()
+    ->fetchCol();
+
+  unset($term_ids);
+
+  return $nids;
+}
+
+/**
+ * Gets term IDs for all descendants of the given term.
+ *
+ * @param int $tid
+ *    The term ID for which to fetch children.
+ *
+ * @return array
+ *    An array of the IDs of the term's descendants.
+ */
+function _taxonomy_access_get_descendants($tid) {
+  $descendants = &drupal_static(__FUNCTION__, array());
+
+  if (!isset($descendants[$tid])) {
+    // Preserve the original state of the list flag.
+    $flag_state = taxonomy_access_list_enabled();
+
+    // Enforce that list grants do not filter the results.
+    taxonomy_access_disable_list();
+
+    $descendants[$tid] = array();
+    $term = taxonomy_term_load($tid);
+    $tree = taxonomy_get_tree($term->vid, $tid);
+
+    foreach ($tree as $term) {
+      $descendants[$tid][] = $term->tid;
+    }
+
+    // Restore list flag to previous state.
+    if ($flag_state) {
+      taxonomy_access_enable_list();
+    }
+
+    unset($term);
+    unset($tree);
+  }
+
+  return $descendants[$tid];
+}
+
+/**
+ * Gets term IDs for all terms in the vocabulary
+ *
+ * @param int $vocab_id
+ *    The vocabulary ID for which to fetch children.
+ *
+ * @return array
+ *    An array of the IDs of the terms in in the vocabulary.
+ */
+function _taxonomy_access_get_vocabulary_terms($vocab_id) {
+  static $descendants = array();
+
+  if (!isset($descendants[$vocab_id])) {
+    // Preserve the original state of the list flag.
+    $flag_state = taxonomy_access_list_enabled();
+
+    // Enforce that list grants do not filter the results.
+    taxonomy_access_disable_list();
+
+    $descendants[$vocab_id] = array();
+    $tree = taxonomy_get_tree($vocab_id);
+
+    foreach ($tree as $term) {
+      $descendants[$vocab_id][] = $term->tid;
+    }
+
+    // Restore list flag to previous state.
+    if ($flag_state) {
+      taxonomy_access_enable_list();
+    }
+
+    unset($term);
+    unset($tree);
+  }
+
+  return $descendants[$vocab_id];
+}
+
+/**
+ * End of "defgroup tac_affected_nodes".
+ * @}
+ */
+
+
+/**
+ * @defgroup tac_grant_api Taxonomy Access Control: Grant record API
+ * @{
+ * Store, retrieve, and delete module access rules for terms and vocabularies.
+ */
+
+
+/**
+ * Deletes module configurations for the given role IDs.
+ *
+ * @param int $rid
+ *   A single role ID.
+ * @param bool $update_nodes
+ *   (optional) A flag to determine whether nodes should be queued for update.
+ *   Defaults to TRUE.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ */
+function taxonomy_access_delete_role_grants($rid, $update_nodes = TRUE) {
+  if (empty($rid)) {
+    return FALSE;
+  }
+  if ($rid == DRUPAL_ANONYMOUS_RID || $rid == DRUPAL_AUTHENTICATED_RID) {
+    return FALSE;
+  }
+
+  if ($update_nodes) {
+    // Cache the list of nodes that will be affected by this change.
+
+    // Affected nodes will be those tied to configurations that are more
+    // permissive than those from the authenticated user role.
+
+    // If any global defaults are more permissive, we need to update all nodes.
+    // Fetch global defaults.
+    $global_defaults = taxonomy_access_global_defaults();
+    $gd_records = array();
+    foreach ($global_defaults as $row) {
+      $gd_records[] = _taxonomy_access_format_node_access_record($row);
+    }
+
+    // Find the ones we need.
+    foreach ($gd_records as $gd) {
+      if ($gd['gid'] == DRUPAL_AUTHENTICATED_RID) {
+        $auth_gd = $gd;
+      }
+      elseif ($gd['gid'] == $rid) {
+        $role_gd = $gd;
+      }
+    }
+
+    // Check node grants for the global default.
+    // If any is more permissive, flag that we need to update all nodes.
+    $all_nodes = FALSE;
+    foreach (array('grant_view', 'grant_update', 'grant_delete') as $op) {
+      switch ($auth_gd[$op]) {
+        // If the authenticated user has a Deny grant, then either Allow or
+        // Ignore for the role is more permissive.
+        case TAXONOMY_ACCESS_NODE_DENY:
+          if (($role_gd[$op] == TAXONOMY_ACCESS_NODE_IGNORE) || ($role_gd[$op] == TAXONOMY_ACCESS_NODE_ALLOW)){
+            $all_nodes = TRUE;
+          }
+          break 2;
+
+        // If the authenticated user has Ignore, Allow is more permissive.
+        case TAXONOMY_ACCESS_NODE_IGNORE:
+          if ($role_gd[$op] == TAXONOMY_ACCESS_NODE_ALLOW) {
+            $all_nodes = TRUE;
+          }
+          break 2;
+      }
+    }
+
+    // If flagged, add all nodes to the affected nodes cache.
+    if ($all_nodes) {
+      $affected_nodes = db_query('SELECT nid FROM {node}')->fetchCol();
+    }
+
+    // Otherwise, just get nodes controlled by specific configurations.
+    else {
+      $affected_nodes =
+        _taxonomy_access_get_controlled_nodes_for_role($rid);
+    }
+    taxonomy_access_affected_nodes($affected_nodes);
+
+    unset($affected_nodes);
+  }
+
+  db_delete('taxonomy_access_term')
+    ->condition('rid', $rid)
+    ->execute();
+
+  db_delete('taxonomy_access_default')
+    ->condition('rid', $rid)
+    ->execute();
+
+  return TRUE;
+}
+
+/**
+ * Deletes module configurations for the given vocabulary IDs.
+ *
+ * @param int|array $vocab_ids
+ *   A single vocabulary ID or an array of vocabulary IDs.
+ * @param int|null $rid
+ *   (optional) A single role ID.  Defaults to NULL.
+ * @param bool $update_nodes
+ *   (optional) A flag to determine whether nodes should be queued for update.
+ *   Defaults to TRUE.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ */
+function taxonomy_access_delete_default_grants($vocab_ids, $rid = NULL, $update_nodes = TRUE) {
+  // Accept either a single vocabulary ID or an array thereof.
+  if ($vocab_ids !== TAXONOMY_ACCESS_GLOBAL_DEFAULT && empty($vocab_ids)) {
+    return FALSE;
+  }
+
+  if ($update_nodes) {
+    // Cache the list of nodes that will be affected by this change.
+    $affected_nodes =
+      _taxonomy_access_get_nodes_for_defaults($vocab_ids, $rid);
+    taxonomy_access_affected_nodes($affected_nodes);
+    unset($affected_nodes);
+  }
+
+  // The query builder will use = or IN() automatically as appropriate.
+  $query =
+    db_delete('taxonomy_access_default')
+    ->condition('vid', $vocab_ids);
+
+  if (!empty($rid)) {
+    $query->condition('rid', $rid);
+  }
+
+  $query->execute();
+  unset($query);
+  return TRUE;
+}
+
+/**
+ * Deletes module configurations for the given term IDs.
+ *
+ * @param int|array $term_ids
+ *   A single term ID or an array of term IDs.
+ * @param int|null $rid
+ *   (optional) A single role ID.  Defaults to NULL.
+ * @param bool $update_nodes
+ *   (optional) A flag to determine whether nodes should be queued for update.
+ *   Defaults to TRUE.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ */
+function taxonomy_access_delete_term_grants($term_ids, $rid = NULL, $update_nodes = TRUE) {
+  // Accept either a single term ID or an array thereof.
+  if (is_numeric($term_ids)) {
+    $term_ids = array($term_ids);
+  }
+
+  if (empty($term_ids)) {
+    return FALSE;
+  }
+
+  if ($update_nodes) {
+    // Cache the list of nodes that will be affected by this change.
+    $affected_nodes = _taxonomy_access_get_nodes_for_terms($term_ids);
+    taxonomy_access_affected_nodes($affected_nodes);
+    unset($affected_nodes);
+  }
+
+  // Delete our database records for these terms.
+  $query =
+    db_delete('taxonomy_access_term')
+    ->condition('tid', $term_ids);
+
+  if (!empty($rid)) {
+    $query->condition('rid', $rid);
+  }
+
+  $query->execute();
+  unset($term_ids);
+  unset($query);
+  return TRUE;
+}
+
+/**
+ * Formats a record to be written to the module's configuration tables.
+ *
+ * @param int $id
+ *   The term or vocabulary ID.
+ * @param int $rid
+ *   The role ID.
+ * @param array $grants
+ *   An array of grants to write, in the format grant_name => value.
+ *   Allowed keys:
+ *   - 'view' or 'grant_view'
+ *   - 'update' or 'grant_update'
+ *   - 'delete' or 'grant_delete'
+ *   - 'create' or 'grant_create'
+ *   - 'list' or 'grant_list'
+ * @param bool $default
+ *   (optional) Whether this is a term record (FALSE) or default record (TRUE).
+ *   Defaults to FALSE.
+ *
+ * @return object
+ *   A grant row object formatted for Schema API.
+ */
+function _taxonomy_access_format_grant_record($id, $rid, array $grants, $default = FALSE) {
+  $row = new stdClass();
+  if ($default) {
+    $row->vid = $id;
+  }
+  else {
+    $row->tid = $id;
+  }
+  $row->rid = $rid;
+  foreach ($grants as $op => $value) {
+    if (is_numeric($value)) {
+      $grant_name = strpos($op, 'grant_') ? $op : "grant_$op";
+      $row->$grant_name = $value;
+    }
+  }
+
+  return $row;
+}
+
+/**
+ * Updates term grants for a role.
+ *
+ * @param array $grant_rows
+ *   An array of grant row objects formatted for Schema API, keyed by term ID.
+ * @param bool $update_nodes
+ *   (optional) A flag indicating whether to update node access.
+ *   Defaults to TRUE.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ *
+ * @see _taxonomy_access_format_grant_record()
+ */
+function taxonomy_access_set_term_grants(array $grant_rows, $update_nodes = TRUE) {
+  // Collect lists of term and role IDs in the list.
+  $terms_for_roles = array();
+  foreach ($grant_rows as $grant_row) {
+    $terms_for_roles[$grant_row->rid][] = $grant_row->tid;
+  }
+
+  // Delete existing records for the roles and terms.
+  // This will also cache a list of the affected nodes.
+  foreach ($terms_for_roles as $rid => $tids) {
+    taxonomy_access_delete_term_grants($tids, $rid, $update_nodes);
+  }
+
+  // Insert new entries.
+  foreach ($grant_rows as $row) {
+    drupal_write_record('taxonomy_access_term', $row);
+  }
+
+  // Later we will refactor; for now return TRUE when this is called.
+  return TRUE;
+}
+
+/**
+ * Updates vocabulary default grants for a role.
+ *
+ * @param $rid
+ *   The role ID to add the permission for.
+ * @param (array) $grant_rows
+ *   An array of grant rows formatted for Schema API, keyed by vocabulary ID.
+ * @param $update_nodes
+ *   (optional) A flag indicating whether to update node access.
+ *   Defaults to TRUE.
+ *
+ * @return bool
+ *   TRUE on success, or FALSE on failure.
+ *
+ * @see _taxonomy_access_format_grant_record()
+ */
+function taxonomy_access_set_default_grants(array $grant_rows, $update_nodes = TRUE) {
+  // Collect lists of term and role IDs in the list.
+  $vocabs_for_roles = array();
+  foreach ($grant_rows as $grant_row) {
+    $vocabs_for_roles[$grant_row->rid][] = $grant_row->vid;
+  }
+
+  // Delete existing records for the roles and vocabularies.
+  // This will also cache a list of the affected nodes.
+  foreach ($vocabs_for_roles as $rid => $vids) {
+    taxonomy_access_delete_default_grants($vids, $rid, $update_nodes);
+  }
+
+  // Insert new entries.
+  foreach ($grant_rows as $row) {
+    drupal_write_record('taxonomy_access_default', $row);
+  }
+
+  // Later we will refactor; for now return TRUE when this is called.
+  return TRUE;
+}
+
+/**
+ * End of "defgroup tac_grant_api".
+ * @}
+ */
+
+/**
+ * @defgroup tac_node_access Taxonomy Access Control: Node access implementation
+ * @{
+ * Functions to set node access based on configured access rules.
+ */
+
+/**
+ * Builds a base query object for the specified TAC grants.
+ *
+ * Callers should add conditions, groupings, and optionally fields.
+ *
+ * This query should work on D7's supported versions of MySQL and PostgreSQL;
+ * patches may be needed for other databases. We add query tags to allow
+ * other systems to manipulate the query as needed.
+ *
+ * @param array $grants
+ *   Grants to select.
+ *   Allowed values: 'view', 'update', 'delete', 'create', 'list'
+ * @param bool $default
+ *   (optional) Flag to select default grants only.  Defaults to FALSE.
+ *
+ * @return object
+ *    Query object.
+ */
+function _taxonomy_access_grant_query(array $grants, $default = FALSE) {
+  $table = $default ? 'taxonomy_vocabulary' : 'taxonomy_term_data';
+  $query =
+    db_select($table, 'td')
+    ->addTag('taxonomy_access')
+    ->addTag('taxonomy_access_grants')
+    ;
+
+  $query->join(
+    'taxonomy_access_default', 'tadg',
+    'tadg.vid = :vid',
+    array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT)
+  );
+  $query->leftJoin(
+    'taxonomy_access_default', 'tad',
+    'tad.vid = td.vid AND tad.rid = tadg.rid'
+  );
+  if (!$default) {
+    $query->leftJoin(
+      'taxonomy_access_term', 'ta',
+      'ta.tid = td.tid AND ta.rid = tadg.rid'
+    );
+  }
+
+  // We add grant fields this way to reduce the risk of future vulnerabilities.
+  $grant_fields = array(
+    'view' => 'grant_view',
+    'update' => 'grant_update',
+    'delete' => 'grant_delete',
+    'create' => 'grant_create',
+    'list' => 'grant_list',
+  );
+
+  foreach ($grant_fields as $name => $grant) {
+    if (in_array($name, $grants)) {
+      if ($default) {
+        $query->addExpression(
+          'BIT_OR(COALESCE('
+          . 'tad.' . db_escape_table($grant) . ', '
+          . 'tadg.' . db_escape_table($grant)
+          . '))',
+          $grant
+        );
+      }
+      else {
+        $query->addExpression(
+          'BIT_OR(COALESCE('
+          . 'ta.' . db_escape_table($grant) . ', '
+          . 'tad.' . db_escape_table($grant) . ', '
+          . 'tadg.' . db_escape_table($grant)
+          . '))',
+          $grant
+        );
+      }
+    }
+  }
+
+  return $query;
+}
+
+/**
+ * Calculates node access grants by role for the given node ID.
+ *
+ * @param $node_nid
+ *   The node ID for which to calculate grants.
+ * @param $reset
+ *   (optional) Whether to recalculate the cached values.  Defaults to FALSE.
+ *
+ * @return
+ *    Array formatted for hook_node_access_records().
+ *
+ * @ingroup tac_node_access
+ */
+function _taxonomy_access_node_access_records($node_nid, $reset = FALSE) {
+
+  // Build the base node grant query.
+  $query = _taxonomy_access_grant_query(array('view', 'update', 'delete'));
+
+  // Select grants for this node only and group by role.
+  $query->join(
+    'taxonomy_index', 'ti',
+    'td.tid = ti.tid'
+  );
+  $query
+    ->fields('tadg', array('rid'))
+    ->condition('ti.nid', $node_nid)
+    ->groupBy('tadg.rid')
+    ->addTag('taxonomy_access_node_access')
+    ->addTag('taxonomy_access_node')
+    ;
+
+  // Fetch and format all grant records for the node.
+  $grants = array();
+  $records = $query->execute()->fetchAll();
+  // The node grant query returns no rows if the node has no tags.
+  // In that scenario, use the global default.
+  if (sizeof($records) == 0) {
+    $records = taxonomy_access_global_defaults($reset);
+  }
+  foreach ($records as $record) {
+    $grants[] = _taxonomy_access_format_node_access_record($record);
+  }
+
+  return $grants;
+}
+
+/**
+ * Returns an array of global default grants for all roles.
+ *
+ * @param bool $reset
+ *   (optional) Whether to recalculate the cached values.  Defaults to FALSE.
+ *
+ * @return array
+ *   An array of global defaults for each role.
+ */
+function taxonomy_access_global_defaults($reset = FALSE) {
+  $global_grants = &drupal_static(__FUNCTION__, array());
+  if (empty($global_grants) || $reset) {
+    $global_grants =
+      db_query(
+        'SELECT rid, grant_view, grant_update, grant_delete, grant_create,
+           grant_list
+         FROM {taxonomy_access_default}
+         WHERE vid = :vid',
+         array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT))
+      ->fetchAllAssoc('rid');
+  }
+  return $global_grants;
+}
+
+/**
+ * Formats a row for hook_node_access_records.
+ *
+ * @param stdClass $record
+ *   The term record object from a TAC query to format.
+ *
+ * @return array
+ *   An array formatted for hook_node_access_records().
+ *
+ * @todo
+ *   Make priority configurable?
+ */
+function _taxonomy_access_format_node_access_record(stdClass $record) {
+
+   // TAXONOMY_ACCESS_NODE_IGNORE => 0, TAXONOMY_ACCESS_NODE_ALLOW => 1,
+   // TAXONOMY_ACCESS_NODE_DENY => 2 ('10' in binary).
+   // Only a value of 1 is considered an 'Allow';
+   // with an 'Allow' and no 'Deny', the value from the BIT_OR will be 1.
+   // If a 'Deny' is present, the value will then be 3 ('11' in binary).
+  return array(
+    'realm' => 'taxonomy_access_role',
+    'gid' => $record->rid,
+    'grant_view' => ($record->grant_view == 1) ? 1 : 0,
+    'grant_update' => ($record->grant_update == 1) ? 1 : 0,
+    'grant_delete' => ($record->grant_delete == 1) ? 1 : 0,
+    'priority' => 0,
+  );
+}
+
+/**
+ * End of "defgroup tac_node_access".
+ * @}
+ */
+
+
+/**
+ * @defgroup tac_list Taxonomy Access Control: View tag (list) permission
+ * @{
+ * Alter queries to control the display of taxonomy terms on nodes and listings.
+ */
+
+
+/**
+ * Flag to disable list grant filtering (e.g., on node edit forms).
+ *
+ * @param bool $set_flag
+ *   (optional) When passed, sets the the flag.  Pass either TRUE or FALSE.
+ *   Defaults to NULL.
+ */
+function _taxonomy_access_list_state($set_flag = NULL) {
+  static $flag = TRUE;
+  // If no flag was passed, return the current state of the flag.
+  if (is_null($set_flag)) {
+    return $flag;
+  }
+  // If we were passed anything but null, set the flag.
+  $flag = $set_flag ? TRUE : FALSE;
+}
+
+/**
+ * Wrapper for taxonomy_access_list_state() to enable list grant filtering.
+ *
+ * @see _taxonomy_access_list_state()
+ */
+function taxonomy_access_enable_list() {
+  _taxonomy_access_list_state(TRUE);
+}
+
+/**
+ * Wrapper for taxonomy_access_list_state() to disable list grant filtering.
+ *
+ * @see _taxonomy_access_list_state()
+ */
+function taxonomy_access_disable_list() {
+  _taxonomy_access_list_state(FALSE);
+}
+
+/**
+ * Wrapper for taxonomy_access_list_state() to check list grant filtering.
+ *
+ * @see _taxonomy_access_list_state()
+ */
+function taxonomy_access_list_enabled() {
+  return _taxonomy_access_list_state();
+}
+
+/**
+ * Retrieve terms that the current user may list.
+ *
+ * @return array|true
+ *   An array of term IDs, or TRUE if the user may list all terms.
+ *
+ * @see _taxonomy_access_user_term_grants()
+ */
+function taxonomy_access_user_list_terms() {
+  // Cache the terms the current user can list.
+  static $terms = NULL;
+  if (is_null($terms)) {
+    $terms = _taxonomy_access_user_term_grants(FALSE);
+  }
+  return $terms;
+}
+
+/**
+ * Retrieve terms that the current user may create or list.
+ *
+ * @param bool $create
+ *   (optional) Whether to fetch grants for create (TRUE) or list (FALSE).
+ *   Defaults to FALSE.
+ * @param array $vids
+ *   (optional) An array of vids to limit the query.  Defaults to array().
+ * @param object|null $account
+ *   (optional) The account for which to retrieve grants.  If no account is
+ *   passed, the current user will be used.  Defaults to NULL.
+ *
+ * @return array|true
+ *   An array of term IDs, or TRUE if the user has the grant for all terms.
+ */
+function _taxonomy_access_user_term_grants($create = FALSE, array $vids = array(), $account = NULL) {
+  $grant_type = $create ? 'create' : 'list';
+  $grant_field_name = 'grant_' . $grant_type;
+
+  // If no account was passed, default to current user.
+  if (is_null($account)) {
+    global $user;
+    $account = $user;
+  }
+
+  // If the user can administer taxonomy, return TRUE for a global grant.
+  if (user_access('administer taxonomy', $account)) {
+    return TRUE;
+  }
+
+  // Build a term grant query.
+  $query = _taxonomy_access_grant_query(array($grant_type));
+
+  // Select term grants for the user's roles.
+  $query
+    ->fields('td', array('tid'))
+    ->groupBy('td.tid')
+    ->condition('tadg.rid', array_keys($account->roles), 'IN')
+    ;
+
+  // Filter by the indicated vids, if any.
+  if (!empty($vids)) {
+    $query
+      ->fields('td', array('vid'))
+      ->condition('td.vid', $vids, 'IN')
+      ;
+  }
+
+  // Fetch term IDs.
+  $r = $query->execute()->fetchAll();
+  $tids = array();
+
+  // If there are results, initialize a flag to test whether the user
+  // has the grant for all terms.
+  $grants_for_all_terms = empty($r) ? FALSE : TRUE;
+
+  foreach ($r as $record) {
+    // If the user has the grant, add the term to the array.
+    if ($record->$grant_field_name) {
+      $tids[] = $record->tid;
+    }
+    // Otherwise, flag that the user does not have the grant for all terms.
+    else {
+      $grants_for_all_terms = FALSE;
+    }
+  }
+
+  // If the user has the grant for all terms, return TRUE for a global grant.
+  if ($grants_for_all_terms) {
+    return TRUE;
+  }
+
+  return $tids;
+}
+
+/**
+ * Field options callback to generate options unfiltered by list grants.
+ *
+ * @param object $field
+ *   The field object.
+ *
+ * @return array
+ *   Allowed terms from taxonomy_allowed_values().
+ *
+ * @see taxonomy_allowed_values()
+ */
+function _taxonomy_access_term_options($field) {
+  // Preserve the original state of the list flag.
+  $flag_state = taxonomy_access_list_enabled();
+
+  // Enforce that list grants do not filter the options list.
+  taxonomy_access_disable_list();
+
+  // Use taxonomy.module to generate the list of options.
+  $options = taxonomy_allowed_values($field);
+
+  // Restore list flag to previous state.
+  if ($flag_state) {
+    taxonomy_access_enable_list();
+  }
+
+  return $options;
+}
+
+/**
+ * End of "defgroup tac_list".
+ * @}
+ */
+
+/**
+ * Form element validation handler for taxonomy autocomplete fields.
+ *
+ * @see taxonomy_access_autocomplete()
+ * @see taxonomy_access_field_widget_taxonomy_autocomplete_form_alter()
+ */
+function taxonomy_access_autocomplete_validate($element, &$form_state) {
+  // Enforce that list grants do not filter this or subsequent validation.
+  taxonomy_access_disable_list();
+
+  // Add create grant handling.
+  module_load_include('inc', 'taxonomy_access', 'taxonomy_access.create');
+  _taxonomy_access_autocomplete_validate($element, $form_state);
+
+}
+
+/**
+ * Form element validation handler for taxonomy options fields.
+ *
+ * @see taxonomy_access_field_widget_form_alter()
+ */
+function taxonomy_access_options_validate($element, &$form_state) {
+  // Enforce that list grants do not filter this or subsequent validation.
+  taxonomy_access_disable_list();
+
+  // Add create grant handling.
+  module_load_include('inc', 'taxonomy_access', 'taxonomy_access.create');
+  _taxonomy_access_options_validate($element, $form_state);
+}
+
+/**
+ * Implements hook_help().
+ */
+function taxonomy_access_help($path, $arg) {
+  switch ($path) {
+    case 'admin/help#taxonomy_access':
+      $message = '';
+      $message .= ''
+        . '<p>' . t('The Taxonomy Access Control module allows users to specify how each category can be used by various roles.') . '</p>'
+        . '<p>' . t('Permissions can be set differently for each user role. Be aware that setting Taxonomy Access permissions works <em>only within one user role</em>.') . '</p>'
+        . '<p>' . t('(For users with multiple user roles, see section <a href="#good-to-know">Good to know</a> below.)') . '</p><hr /><br />'
+        . "<h3>" . t("On this page") . "</h3>"
+        . "<ol>"
+        . '<li><a href="#grant">' . t("Grant types") . '</a></li>'
+        . '<li><a href="#perm">' . t("Permission options") . '</a></li>'
+        . '<li><a href="#defaults">' . t("Global and vocabulary defaults") . '</a></li>'
+        . '<li><a href="#good-to-know">' . t("Good to know") . '</a></li>'
+        . "</ol><hr /><br />"
+        . '<h3 id="grant">' . t("Grant types") . '</h3>'
+        . '<p>' . t('On the category permissions page for each role, administrators can configure five types of permission for each term: <em>View, Update, Delete, Add Tag</em> (formerly <em>Create</em>), and <em>View Tag</em>: (formerly <em>List</em>') . '</p>'
+        . _taxonomy_access_grant_help_table()
+        . '<p>' . t('<em>View</em>, <em>Update</em>, and <em>Delete</em> control the node access system.  <em>View Tag</em> and <em>Add Tag</em> control the terms themselves.  (Note: In previous versions of Taxonomy Access Control, there was no <em>View Tag</em> permission its functionality was controlled by the <em>View</em> permission.)') . '</p><hr /><br />'
+        . '<h3 id="perm">' . t("Permission options") . "</h3>"
+        . '<p>' . t('<strong><em>View</em>, <em>Update</em>, and <em>Delete</em> have three options for each term:</strong> <em>Allow</em> (<acronym title="Allow">A</acronym>), <em>Ignore</em> (<acronym title="Ignore">I</acronym>), and <em>Deny</em> (<acronym title="Deny">D</acronym>).  Indicate which rights each role should have for each term.  If a node is tagged with multiple terms:') . '</p>'
+        . "<ul>\n"
+        . "<li>"
+        . t('<em>Deny</em> (<acronym title="Deny">D</acronym>) overrides <em>Allow</em> (<acronym title="Allow">A</acronym>) within a role.')
+        . "</li>"
+        . "<li>"
+        . t('Both <em>Allow</em> (<acronym title="Allow">A</acronym>) and <em>Deny</em> (<acronym title="Deny">D</acronym>) override <em>Ignore</em> (<acronym title="Ignore">I</acronym>) within a role.')
+        . "</li>"
+        . "<li>"
+        . t('If a user has <strong>multiple roles</strong>, an <em>Allow</em> (<acronym title="Allow">A</acronym>) from one role <strong>will</strong> override a <em>Deny</em> (<acronym title="Deny">D</acronym>) in another.  (For more information, see section <a href="#good-to-know">Good to know</a> below.)')
+        . "</li>"
+        . "</ul>\n\n"
+        . '<p>' . t('<strong><em>Add Tag</em> and <em>View Tag</em> have only two options for each term:</strong>  <em>Yes</em> (selected) or <em>No</em> (deselected).  Indicate what each role should be allowed to do with each term.') . '</p>'
+        . "<h4>" . t("Important notes") . "</h4>"
+        . "<ol>"
+        . "<li>"
+        . t('Custom roles <strong>will</strong> inherit permissions from the <em>authenticated user</em> role.  Be sure to <a href="@url">configure
+the authenticated user</a> properly.',
+          array("@url" => url(
+              TAXONOMY_ACCESS_CONFIG
+              . '/role/'
+              . DRUPAL_AUTHENTICATED_RID
+              . 'edit')))
+        . "</li>\n"
+        . '<li>'
+        . "<p>" . t('The <em>Deny</em> directives are processed after the <em>Allow</em> directives. (<strong><em>Deny</em> overrides <em>Allow</em></strong>.)</em>  So, if a multicategory node is in Categories "A" and "B" and a user has <em>Allow</em> permissions for <em>View</em> in Category "A" and <em>Deny</em> permissions for <em>View</em> in Category "B", then the user will NOT be permitted to <em>View</em> the node.') . '</p>'
+        . '<p>' . t('<em>Access is denied by default.</em> So, if a multicategory node is in Categories "C" and "D" and a user has <em>Ignore</em> permissions for <em>View</em> in both Category "C" and "D", then the user will <strong>not</strong> be permitted to view the node.') . '</p>'
+        . '<p>' . t('(If you are familiar with Apache mod_access, this permission system works similar to directive: <em>ORDER ALLOW, DENY</em>)') . '</p>'
+        . "</li>"
+        . "</ol>"
+        . "<hr /><br />"
+        . '<h3 id="defaults">' . t("Global and vocabulary defaults") . "</h3>"
+        . '<p>' . t('This option, just underneath the vocabulary title, <em>sets the permission that will automatically be given</em> to the role, <em>for any new terms</em> that are added within the vocabulary.  This includes terms that are added via free tagging.') . '</p><hr /><br />'
+        . '<h3 id="good-to-know">' . t('Good to know') . '</h3>'
+        . '<ol>'
+        . '<li>'
+        . '<p>' . t('<strong>Users with multiple user roles:</strong> Allow/Ignore/Deny options are interpreted <em>only within one user role</em>. When a user belongs to multiple user roles, then <strong>the user gets access if <em>any</em> of his/her user roles have the access granted.</strong>') . '</p>'
+        . '<p>' . t('In this case, permissions for the given user are calculated so that the <em>permissions of ALL of his user roles are "OR-ed" together</em>, which means that <em>Allow</em> in one role will take precedence over <em>Deny</em> in the other. This is different from how node access permissions (for multi-category nodes) are handled <em>within one user role</em>, as noted above.') . '</p>'
+        . '</li>'
+        . '<li>'
+        . '<p>' . t('<strong>Input formats:</strong>  <em>Node editing/deleting is blocked</em>, even when the user has <em>Update</em> or <em>Delete</em> permission to the node, <em>when the user is not allowed to use a filter format</em> that the node was saved at.') . '</p>'
+        . '</li>'
+        . '</ol>'
+        . '<hr /><br />'
+        ;
+      return $message;
+      break;
+  }
+}
+
+/**
+ * Assembles a table explaining each grant type for use in help documentation.
+ *
+ * @return string
+ *   Themed table.
+ *
+ * @todo
+ *   We moved this here for drush.  Find a smarter way to include it on demand?
+ */
+function _taxonomy_access_grant_help_table() {
+  $header = array();
+
+  $rows = array();
+  $rows[] = array(
+    array('header' => TRUE, 'data' => t("View")),
+    "<p>"
+    . t('Grants this role the ability to view nodes with the term.  (Users must also have this permission to see <em class="perm">nodes</em> with the term listed in Views.)')
+    . "</p>"
+    . "<p>"
+    . t('The role must <strong>have</strong> <em class="perm">access content</em> permission on the <a href="@path">permissions administration form</a>.',
+      array('@path' => url('admin/people/permissions', array('fragment' => 'module-node')))),
+  );
+
+  $rows[] = array(
+    array('header' => TRUE, 'data' => t("Update") . ", " . t("Delete")),
+    "<p>"
+    . t("Grants this role the ability to edit or delete nodes with the term, respectively.")
+    . "</p>"
+    . "<p>"
+    . t('The role must <strong>not</strong> have <em class="perm">edit any [type] content</em> or <em class="perm">delete any [type] content</em> permission on the <a href="@path">permissions administration form</a> if you wish to control them here.',
+      array('@path' => url('admin/people/permissions', array('fragment' => 'module-node'))))
+    . "</p>",
+  );
+
+  $rows[] = array(
+    array('header' => TRUE, 'data' => t("Add Tag")),
+    "<p>"
+    . t("Grants this role the ability to add the term to a node when creating or updating it.")
+    . "</p>"
+    . "<p>"
+    . t('(Formerly <em>Create</em>).  This does <strong>not</strong> give the role the ability to create nodes by itself; the role must <strong>have</strong> <em class="perm">create [type] content</em> permission on the <a href="@path">permissions administration form</a> in order to create new nodes.',
+      array('@path' => url('admin/people/permissions', array('fragment' => 'module-node'))))
+    . "</p>",
+  );
+
+  $rows[] = array(
+    array('header' => TRUE, 'data' => t("View Tag")),
+    "<p>"
+    . t("(Formerly <em>List</em>.)  Whether this role can see the term listed on node pages and in lists, and whether the user can view the %taxonomy-term-page page for the term.",
+      array(
+        '%taxonomy-term-page' => "taxonomy/term/x"
+      ))
+    . "</p>"
+    . "<p>" . t("This does <strong>not</strong> control whether the role can see the <em>nodes</em> listed in Views, only the <em>term</em>.") . "</p>",
+  );
+
+  return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('grant_help'))));
+}
+
+/**
+ * Implements hook_disable().
+ *
+ * Removes all options_list callbacks during disabling of the module which were
+ * set in taxonomy_access_field_info_alter().
+ */
+function taxonomy_access_disable() {
+  foreach (field_read_fields() as $field_name => $field) {
+    if ($field['type'] == 'taxonomy_term_reference') {
+      if (!empty($field['settings']['options_list_callback']) && $field['settings']['options_list_callback'] == '_taxonomy_access_term_options') {
+        $field['settings']['options_list_callback'] = '';
+        field_update_field($field);
+      }
+    }
+  }
+}

+ 1620 - 0
sites/all/modules/contrib/taxonomy/taxonomy_access/taxonomy_access.test

@@ -0,0 +1,1620 @@
+<?php
+
+/**
+ * @file
+ * Automated tests for the Taxonomy Access Control module.
+ */
+
+/**
+ * Provides a base test class and helper methods for automated tests.
+ */
+class TaxonomyAccessTestCase extends DrupalWebTestCase {
+  // There are four types of users:
+  // site admins, taxonomy admins, content editors, and regular users.
+  protected $users = array();
+  protected $user_roles = array();
+  protected $user_config = array(
+    'site_admin' => array(
+      'access content',
+      'access site reports',
+      'access administration pages',
+      'administer permissions',
+      'create article content',
+      'edit any article content',
+      'create page content',
+      'edit any page content',
+    ),
+    'tax_admin' => array(
+      'access content',
+      'administer taxonomy',
+    ),
+    'editor' => array(
+      'access content',
+      'create article content',
+      'create page content',
+    ),
+    'regular_user' =>
+      array(
+        'access content',
+      ),
+  );
+
+  public function setUp() {
+    // Enable module and dependencies.
+    parent::setUp('taxonomy_access');
+
+    // Rebuild node access on installation.
+    node_access_rebuild();
+
+    // Configure users with base permission patterns.
+    foreach ($this->user_config as $user => $permissions) {
+      $this->users[$user] = $this->drupalCreateUser($permissions);
+
+      // Save the role ID separately so it's easy to retrieve.
+      foreach ($this->users[$user]->roles as $rid => $role) {
+        if ($rid != DRUPAL_AUTHENTICATED_RID) {
+          $this->user_roles[$user] = user_role_load($rid);
+        }
+      }
+    }
+
+    // Give the anonymous and authenticated roles ignore grants.
+    $rows = array();
+    foreach (array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID) as $rid) {
+      $ignore = array(
+        'view' => TAXONOMY_ACCESS_NODE_IGNORE,
+        'update' => TAXONOMY_ACCESS_NODE_IGNORE,
+        'delete' => TAXONOMY_ACCESS_NODE_IGNORE,
+      );
+      $rows[] = _taxonomy_access_format_grant_record(TAXONOMY_ACCESS_GLOBAL_DEFAULT, $rid, $ignore, TRUE);
+    }
+    taxonomy_access_set_default_grants($rows);
+
+    foreach (array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID) as $rid) {
+      $r =
+        db_query(
+          'SELECT grant_view FROM {taxonomy_access_default}
+           WHERE vid = :vid AND rid = :rid',
+          array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT, ':rid' => $rid)
+        )
+        ->fetchField();
+      $this->assertTrue(is_numeric($r) && $r == 0, t("Set global default for role %rid to <em>Ignore</em>", array('%rid' => $rid)));
+    }
+  }
+
+  /**
+   * Creates a vocabulary with a certain name.
+   *
+   * @param string $machine_name
+   *   A machine-safe name.
+   *
+   * @return object
+   *   The vocabulary object.
+   */
+  function createVocab($machine_name) {
+    $vocabulary = new stdClass();
+    $vocabulary->name = $machine_name;
+    $vocabulary->description = $this->randomName();
+    $vocabulary->machine_name = $machine_name;
+    $vocabulary->help = '';
+    $vocabulary->weight = mt_rand(0, 10);
+    taxonomy_vocabulary_save($vocabulary);
+    return $vocabulary;
+  }
+
+  /**
+   * Creates a new term in the specified vocabulary.
+   *
+   * @param string $machine_name
+   *   A machine-safe name.
+   * @param object $vocab
+   *   A vocabulary object.
+   * @param int|null $parent
+   *   (optional) The tid of the parent term, if any.  Defaults to NULL.
+   *
+   * @return object
+   *   The taxonomy term object.
+   */
+  function createTerm($machine_name, $vocab, $parent = NULL) {
+    $term = new stdClass();
+    $term->name = $machine_name;
+    $term->description = $machine_name;
+    // Use the first available text format.
+    $term->format =
+      db_query_range('SELECT format FROM {filter_format}', 0, 1)->fetchField();
+    $term->vid = $vocab->vid;
+    $term->vocabulary_machine_name = $vocab->machine_name;
+    if (!is_null($parent)) {
+      $term->parent = $parent;
+    }
+    taxonomy_term_save($term);
+    return $term;
+  }
+
+  /**
+   * Creates a taxonomy field and adds it to the page content type.
+   *
+   * @param string $machine_name
+   *   The machine name of the vocabulary to use.
+   * @param string $widget
+   *   (optional) The name of the widget to use.  Defaults to 'options_select'.
+   * @param int $count
+   *   (optional) The allowed number of values.  Defaults to unlimited.
+   *
+   * @return array
+   *   Array of instance data.
+   */
+  function createField($machine_name, $widget = 'options_select', $count = FIELD_CARDINALITY_UNLIMITED) {
+    $field = array(
+      'field_name' => $machine_name,
+      'type' => 'taxonomy_term_reference',
+      'cardinality' => $count,
+      'settings' => array(
+        'allowed_values' => array(
+          array(
+            'vocabulary' => $machine_name,
+            'parent' => 0,
+          ),
+        ),
+      ),
+    );
+    $field = field_create_field($field);
+
+    $instance = array(
+      'field_name' => $machine_name,
+      'bundle' => 'page',
+      'entity_type' => 'node',
+      'widget' => array(
+        'type' => $widget,
+      ),
+      'display' => array(
+        'default' => array(
+          'type' => 'taxonomy_term_reference_link',
+        ),
+      ),
+    );
+
+    return field_create_instance($instance);
+  }
+
+ /**
+   * Creates an article with the specified terms.
+   *
+   * @param array $autocreate
+   *   (optional) An array of term names to autocreate. Defaults to array().
+   * @param array $existing
+   *   (optional) An array of existing term IDs to add.
+   *
+   * @return object
+   *   The node object.
+   */
+  function createArticle($autocreate = array(), $existing = array()) {
+    $values = array();
+    foreach ($autocreate as $name) {
+      $values[] = array('tid' => 'autocreate', 'vid' => 1, 'name' => $name, 'vocabulary_machine_name' => 'tags');
+    }
+    foreach ($existing as $tid) {
+      $values[] = array('tid' => $tid, 'vid' => 1, 'vocabulary_machine_name' => 'tags');
+    }
+
+    // Bloody $langcodes.
+    $values = array(LANGUAGE_NONE => $values);
+
+    $settings = array(
+      'type' => 'article',
+      'field_tags' => $values,
+    );
+
+    return $this->drupalCreateNode($settings);
+  }
+
+  /**
+   * Submits the node access rebuild form.
+   */
+  function rebuild() {
+    $this->drupalPost('admin/reports/status/rebuild', array(), t('Rebuild permissions'));
+    $this->assertText(t('The content access permissions have been rebuilt.'));
+  }
+
+  /**
+   * Asserts that a status column and "Configure" link is found for the role.
+   *
+   * @param array $statuses
+   *   An associative array of role statuses, keyed by role ID. Each item
+   *   should be TRUE if the role is enabled, and FALSE otherwise.
+   */
+  function checkRoleConfig(array $statuses) {
+    $roles = _taxonomy_access_user_roles();
+
+    // Log in as the administrator.
+    $this->drupalLogout();
+    $this->drupalLogin($this->users['site_admin']);
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG);
+
+    foreach ($statuses as $rid => $status) {
+      // Assert that a "Configure" link is available for the role.
+      $this->assertLinkByHref(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit",
+        0,
+        t('"Configure" link is available for role %rid.', array('%rid' => $rid)));
+    }
+
+    // Retrieve the grant status table.
+    $shown = array();
+    $table = $this->xpath('//table/tbody');
+    $table = reset($table);
+    // SimpleXML has fake arrays so we have to do this to get the data out.
+    foreach ($table->tr as $row) {
+      $tds = array();
+      foreach ($row->td as $value) {
+        $tds[] = (string) $value;
+      }
+      $shown[$tds[0]] = $tds[1];
+    }
+
+    foreach ($statuses as $rid => $status) {
+      // Assert that the form shows the passed status.
+      if ($status) {
+        $this->assertTrue(
+          $shown[$roles[$rid]] == t('Enabled'),
+          format_string('Role %role is enabled.', array('%role' => $rid)));
+      }
+      else {
+        $this->assertTrue(
+          $shown[$roles[$rid]] == t('Disabled'),
+          format_string('Role %role is disabled.', array('%role' => $rid)));
+      }
+
+      // Assert that a "Configure" link is available for the role.
+      $this->assertLinkByHref(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit",
+        0,
+        t('"Configure" link is available for role %rid.',
+          array('%rid' => $rid)));
+    }
+
+  }
+
+  /**
+   * Asserts that an enable link is or is not found for the role.
+   *
+   * @param int $rid
+   *   The role ID to check.
+   * @param bool $found
+   *   Whether the link should be found, or not.
+   */
+  function checkRoleEnableLink($rid, $found) {
+    if ($found) {
+      $this->assertLinkByHref(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/enable",
+        0,
+        t('Enable link is available for role %rid.', array('%rid' => $rid))
+      );
+    }
+    else {
+      $this->assertNoLinkByHref(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/enable",
+        t('Enable link is not available for role %rid.', array('%rid' => $rid))
+      );
+    }
+  }
+
+  /**
+   * Asserts that a disable link is or is not found for the role.
+   *
+   * @param int $rid
+   *   The role ID to check.
+   * @param bool $found
+   *   Whether the link should be found, or not.
+   */
+  function checkRoleDisableLink($rid, $found) {
+    if ($found) {
+      $this->assertLinkByHref(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/delete",
+        0,
+        t('Disable link is available for role %rid.', array('%rid' => $rid))
+      );
+    }
+    else {
+      $this->assertNoLinkByHref(
+        TAXONOMY_ACCESS_CONFIG . "/role/$rid/delete",
+        t('Disable link is not available for role %rid.', array('%rid' => $rid))
+      );
+    }
+  }
+
+  /**
+   * Adds a term row on the role configuration form.
+   *
+   * @param array &$edit
+   *   The form data to post.
+   * @param int $vid
+   *   (optional) The vocabulary ID. Defaults to
+   *   TAXONOMY_ACCESS_GLOBAL_DEFAULT.
+   * @param $int tid
+   *   (optional) The term ID. Defaults to TAXONOMY_ACCESS_VOCABULARY_DEFAULT.
+   * @param int $view
+   *   (optional) The view grant value. Defaults to
+   *    TAXONOMY_ACCESS_NODE_IGNORE.
+   * @param int $update
+   *   (optional) The update grant value. Defaults to
+   * @param int $delete
+   *   (optional) The delete grant value. Defaults to
+   *   TAXONOMY_ACCESS_NODE_IGNORE.
+   * @param int $create
+   *   (optional) The create grant value. Defaults to
+   *   TAXONOMY_ACCESS_TERM_DENY.
+   * @param int $list
+   *   (optional) The list grant value. Defaults to TAXONOMY_ACCESS_TERM_DENY.
+   */
+  function addFormRow(&$edit,  $vid = TAXONOMY_ACCESS_GLOBAL_DEFAULT, $tid = TAXONOMY_ACCESS_VOCABULARY_DEFAULT, $view = TAXONOMY_ACCESS_NODE_IGNORE, $update = TAXONOMY_ACCESS_NODE_IGNORE, $delete = TAXONOMY_ACCESS_NODE_IGNORE, $create = TAXONOMY_ACCESS_TERM_DENY, $list = TAXONOMY_ACCESS_TERM_DENY) {
+    $new_value = $tid ? "term $tid" : "default $vid";
+    $edit["new[$vid][item]"] = $new_value;
+    $edit["new[$vid][grants][$vid][0][view]"] = $view;
+    $edit["new[$vid][grants][$vid][0][update]"] = $update;
+    $edit["new[$vid][grants][$vid][0][delete]"] = $delete;
+    $edit["new[$vid][grants][$vid][0][create]"] = $create;
+    $edit["new[$vid][grants][$vid][0][list]"] = $list;
+  }
+
+  /**
+   * Configures a row on the TAC configuration form.
+   *
+   * @param array &$edit
+   *   The form data to post.
+   * @param int $vid
+   *   (optional) The vocabulary ID. Defaults to
+   *   TAXONOMY_ACCESS_GLOBAL_DEFAULT.
+   * @param $int tid
+   *   (optional) The term ID. Defaults to TAXONOMY_ACCESS_VOCABULARY_DEFAULT.
+   * @param int $view
+   *   (optional) The view grant value. Defaults to
+   *    TAXONOMY_ACCESS_NODE_IGNORE.
+   * @param int $update
+   *   (optional) The update grant value. Defaults to
+   * @param int $delete
+   *   (optional) The delete grant value. Defaults to
+   *   TAXONOMY_ACCESS_NODE_IGNORE.
+   * @param int $create
+   *   (optional) The create grant value. Defaults to
+   *   TAXONOMY_ACCESS_TERM_DENY.
+   * @param int $list
+   *   (optional) The list grant value. Defaults to TAXONOMY_ACCESS_TERM_DENY.
+   */
+  function configureFormRow(&$edit,  $vid = TAXONOMY_ACCESS_GLOBAL_DEFAULT, $tid = TAXONOMY_ACCESS_VOCABULARY_DEFAULT, $view = TAXONOMY_ACCESS_NODE_IGNORE, $update = TAXONOMY_ACCESS_NODE_IGNORE, $delete = TAXONOMY_ACCESS_NODE_IGNORE, $create = TAXONOMY_ACCESS_TERM_DENY, $list = TAXONOMY_ACCESS_TERM_DENY) {
+    $edit["grants[$vid][$tid][view]"] = $view;
+    $edit["grants[$vid][$tid][update]"] = $update;
+    $edit["grants[$vid][$tid][delete]"] = $delete;
+    $edit["grants[$vid][$tid][create]"] = $create;
+    $edit["grants[$vid][$tid][list]"] = $list;
+  }
+}
+
+/**
+ * Tests the module's response to changes from other modules.
+ */
+class TaxonomyAccessExternalChanges extends TaxonomyAccessTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'External changes',
+      'description' => "Test the module's response to changes from other modules.",
+      'group' => 'Taxonomy Access Control',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+  }
+
+  /*
+1. delete a term
+2. delete a role
+3. delete a field attachment
+4. modify a field attachment
+5. delete a vocabulary
+6. add terms to node
+7. remove terms from node
+  */
+}
+
+/**
+ * Tests the module's configuration forms.
+ */
+class TaxonomyAccessConfigTest extends TaxonomyAccessTestCase {
+  protected $articles = array();
+  protected $pages = array();
+  protected $vocabs = array();
+  protected $terms = array();
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Configuration forms',
+      'description' => 'Test module configuration forms.',
+      'group' => 'Taxonomy Access Control',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    // Add two taxonomy fields to pages.
+    foreach (array('v1', 'v2') as $vocab) {
+      $this->vocabs[$vocab] = $this->createVocab($vocab);
+      $this->createField($vocab);
+      $this->terms[$vocab . 't1'] =
+        $this->createTerm($vocab . 't1', $this->vocabs[$vocab]);
+      $this->terms[$vocab . 't2'] =
+        $this->createTerm($vocab . 't2', $this->vocabs[$vocab]);
+    }
+
+    // Set up a variety of nodes with different term combinations.
+    $this->articles['no_tags'] = $this->createArticle();
+    $this->articles['one_tag'] =
+      $this->createArticle(array($this->randomName()));
+    $this->articles['two_tags'] =
+      $this->createArticle(array($this->randomName(), $this->randomName()));
+
+    $this->pages['no_tags'] = $this->createPage();
+    foreach ($this->terms as $t1) {
+      $this->pages[$t1->name] = $this->createPage(array($t1->name));
+      foreach ($this->terms as $t2) {
+        $this->pages[$t1->name . '_' . $t2->name] =
+          $this->createPage(array($t1->name, $t2->name));
+      }
+    }
+  }
+
+  /**
+   * Creates a page with the specified terms.
+   *
+   * @param array $terms
+   *   (optional) An array of term names to tag the page.  Defaults to array().
+   *
+   * @return object
+   *   The node object.
+   */
+  function createPage($tags = array()) {
+    $v1 = array();
+    $v2 = array();
+
+    foreach ($tags as $name) {
+      switch ($this->terms[$name]->vid) {
+        case ($this->vocabs['v1']->vid):
+          $v1[] = array('tid' => $this->terms[$name]->tid);
+          break;
+
+        case ($this->vocabs['v2']->vid):
+          $v2[] = array('tid' => $this->terms[$name]->tid);
+          break;
+      }
+    }
+
+    // Bloody $langcodes.
+    $v1 = array(LANGUAGE_NONE => $v1);
+    $v2 = array(LANGUAGE_NONE => $v2);
+
+    $settings = array(
+      'type' => 'page',
+      'v1' => $v1,
+      'v2' => $v2,
+    );
+
+    return $this->drupalCreateNode($settings);
+  }
+
+/*
+@todo
+- check anon and auth forms
+- add recursive for vocab and for term
+- change multiple
+- delete multiple
+- configure create and list
+ */
+
+  /**
+   * Tests the initial state of the test environment.
+   *
+   * Verifies that:
+   * - Access to all nodes is denied for anonymous users.
+   * - The main admin page provides the correct configuration links.
+   */
+  public function testSetUpCheck() {
+    // Visit all nodes as anonymous and verify that access is denied.
+    foreach ($this->articles as $key => $article) {
+      $this->drupalGet('node/' . $article->nid);
+      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
+    }
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+    }
+
+    // Log in as the regular_user.
+    $this->drupalLogin($this->users['regular_user']);
+
+    // Visit all nodes and verify that access is denied.
+    foreach ($this->articles as $key => $article) {
+      $this->drupalGet('node/' . $article->nid);
+      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
+    }
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+    }
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Confirm that only edit links are available for anon. and auth.
+    $this->checkRoleConfig(array(
+      DRUPAL_ANONYMOUS_RID => TRUE,
+      DRUPAL_AUTHENTICATED_RID => TRUE,
+    ));
+  }
+
+  /**
+   * Tests configuring a global default.
+   *
+   * Verifies that:
+   * - Access is updated for all nodes when there are no other configurations.
+   * - Access is updated for the correct nodes when there are specific term
+   *    and vocabulary configurations.
+   */
+  public function testGlobalDefaultConfig() {
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the admin form to give anonymous view allow in the global default.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $this->configureFormRow($edit, TAXONOMY_ACCESS_GLOBAL_DEFAULT, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
+    $this->drupalPost(NULL, $edit, 'Save all');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each node and verify that access is allowed.
+    foreach ($this->articles as $key => $article) {
+      $this->drupalGet('node/' . $article->nid);
+      $this->assertResponse(200, t("Access to %name article (nid %nid) is allowed.", array('%name' => $key, '%nid' => $article->nid)));
+    }
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+      $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+    }
+
+    // Add some specific configurations programmatically.
+
+    // Set the v1 default to view allow.
+    $default_config = _taxonomy_access_format_grant_record(
+      $this->vocabs['v1']->vid, DRUPAL_ANONYMOUS_RID, array('view' => TAXONOMY_ACCESS_NODE_ALLOW), TRUE
+    );
+    taxonomy_access_set_default_grants(array($default_config));
+
+    // Set v1t1 and v2t1 to view allow.
+    $term_configs = array();
+    foreach (array('v1t1', 'v2t1') as $name) {
+      $term_configs[] = _taxonomy_access_format_grant_record(
+        $this->terms[$name]->vid, DRUPAL_ANONYMOUS_RID, array('view' => TAXONOMY_ACCESS_NODE_ALLOW)
+      );
+    }
+    taxonomy_access_set_term_grants($term_configs);
+
+    // This leaves articles and the v2t2 page controlled by the global default.
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the admin form to give anonymous view deny in the global default.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $this->configureFormRow($edit, TAXONOMY_ACCESS_GLOBAL_DEFAULT, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_DENY);
+    $this->drupalPost(NULL, $edit, 'Save all');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each artile and verify that access is denied.
+    foreach ($this->articles as $key => $article) {
+      $this->drupalGet('node/' . $article->nid);
+      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
+    }
+
+    // Visit each page.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      switch (TRUE) {
+        // If the page has no tags, access should be denied.
+        case ($key == 'no_tags'):
+        // If the page is tagged with v2t2, access should be denied.
+        case (strpos($key, 'v2t2') !== FALSE):
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Otherwise, access should be allowed.
+        default:
+          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+      }
+    }
+  }
+
+  /**
+   * Tests configuring vocabulary defaults.
+   *
+   * Verifies that:
+   * - Access is updated correctly when the vocabulary default is added and
+   *   configured.
+   * - Access is updated correctly when there is a specific term configuration
+   *   in the vocabulary.
+   * - Access is updated correctly when multiple defaults are changed.
+   * - Access is updated correctly when the vocabulary default is deleted.
+   */
+  public function testVocabularyDefaultConfig() {
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Enable the vocabulary.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    // @todo
+    //   - Ensure that all vocabularies are options in the "Add" fieldset.
+    $edit = array();
+    $edit['enable_vocab'] = $this->vocabs['v1']->vid;
+    $this->drupalPost(NULL, $edit, t('Add'));
+
+    // @todo
+    //   - Ensure that the vocabulary is removed from the "Add" fieldset.
+    //   - Ensure that the fieldset for the vocabulary appears.
+    //   - Ensure that no other fieldsets or rows appear.
+
+    // Give anonymous view allow for the v1 default.
+    $edit = array();
+    $this->configureFormRow($edit, $this->vocabs['v1']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
+    $this->drupalPost(NULL, $edit, 'Save all');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      // If the page is tagged with a v1 term, access should be allowed.
+      if (strpos($key, 'v1') !== FALSE) {
+        $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+      }
+      // Otherwise, access should be denied.
+      else {
+        $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+      }
+    }
+
+    // Programmatically enable v2 and add a specific configuration for v2t1.
+    taxonomy_access_enable_vocab($this->vocabs['v2']->vid, DRUPAL_ANONYMOUS_RID);
+    $term_config = _taxonomy_access_format_grant_record(
+      $this->terms['v2t1']->tid, DRUPAL_ANONYMOUS_RID, array('view' => TAXONOMY_ACCESS_NODE_IGNORE)
+    );
+    taxonomy_access_set_term_grants(array($term_config));
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the admin form to give anonymous view deny for the v2 default.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $this->configureFormRow($edit, $this->vocabs['v2']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_DENY);
+    $this->drupalPost(NULL, $edit, 'Save all');
+
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+
+    // Log out.
+    $this->drupalLogout();
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      switch (TRUE) {
+        // If the page is tagged with v2t2, the v2 default is inherited: Deny.
+        case (strpos($key, 'v2t2') !== FALSE):
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Otherwise, if the page is tagged with v1, it's allowed.
+        case (strpos($key, 'v1') !== FALSE):
+          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Access should be denied by default.
+        default:
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+      }
+    }
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the form to change the configuration: Allow for v2; Deny for v1.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $this->configureFormRow($edit, $this->vocabs['v2']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
+    $this->configureFormRow($edit, $this->vocabs['v1']->vid, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_DENY);
+    $this->drupalPost(NULL, $edit, 'Save all');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      switch (TRUE) {
+        // If the page is tagged with a v1 term, access should be denied.
+        case (strpos($key, 'v1') !== FALSE):
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Otherwise, if the page is tagged with v2t2, the default is
+        // inherited and access should be allowed.
+        case (strpos($key, 'v2t2') !== FALSE):
+          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Access should be denied by default.
+        default:
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+      }
+    }
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the admin form to disable v1.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $this->clickLink(t('delete all v1 access rules'));
+    $this->assertText("Are you sure you want to delete all Taxonomy access rules for v1", t('Disable form for vocabulary loaded.'));
+    $this->drupalPost(NULL, array(), 'Delete all');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      // If the page is tagged with v2t2, access should be allowed.
+      if (strpos($key, 'v2t2') !== FALSE) {
+        $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+      }
+      // Otherwise, access should be denied.
+      else {
+        $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+      }
+    }
+  }
+
+  /**
+   * Tests configuring specific terms.
+   *
+   * Verifies that:
+   * - Access is updated correctly when the term configuration is added.
+   * - Access is updated correctly when there is a vocabulary default.
+   * - Access is updated correctly when multiple configurations are changed.
+   * - Access is updated correctly when the term configuration is deleted.
+   */
+  public function testTermConfig() {
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the admin form to enable v1 and give anonymous view allow for v1t1.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $edit['enable_vocab'] = $this->vocabs['v1']->vid;
+    $this->drupalPost(NULL, $edit, t('Add'));
+    $edit = array();
+    $this->addFormRow($edit, $this->vocabs['v1']->vid, $this->terms['v1t1']->tid, TAXONOMY_ACCESS_NODE_ALLOW);
+    $this->drupalPost(NULL, $edit, 'Add');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      // If the page is tagged with v1t1, access should be allowed.
+      if (strpos($key, 'v1t1') !== FALSE) {
+        $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+      }
+      // Otherwise, access should be denied.
+      else {
+        $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+      }
+    }
+
+    // Enable v2 programmatically.
+    taxonomy_access_enable_vocab($this->vocabs['v2']->vid, DRUPAL_ANONYMOUS_RID);
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the admin form to give anonymous view deny for v2t1.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $this->addFormRow($edit, $this->vocabs['v2']->vid, $this->terms['v2t1']->tid, TAXONOMY_ACCESS_NODE_DENY);
+    $this->drupalPost(NULL, $edit, 'Add');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      switch (TRUE) {
+        // If the page is tagged with v2t1, access should be denied.
+        case (strpos($key, 'v2t1') !== FALSE):
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Otherwise, if the page is tagged with v1t1, it's allowed.
+        case (strpos($key, 'v1t1') !== FALSE):
+          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Access should be denied by default.
+        default:
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+      }
+    }
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the form to change the configuration: Allow for v2t1; Deny for v1t1.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $this->configureFormRow(
+      $edit, $this->vocabs['v2']->vid, $this->terms['v2t1']->tid, TAXONOMY_ACCESS_NODE_ALLOW
+    );
+    $this->configureFormRow(
+      $edit, $this->vocabs['v1']->vid, $this->terms['v1t1']->tid, TAXONOMY_ACCESS_NODE_DENY
+    );
+    $this->drupalPost(NULL, $edit, 'Save all');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      switch (TRUE) {
+        // If the page is tagged with v1t1, access should be denied.
+        case (strpos($key, 'v1t1') !== FALSE):
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Otherwise, if the page is tagged with v2t1, it's allowed.
+        case (strpos($key, 'v2t1') !== FALSE):
+          $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+
+        // Access should be denied by default.
+        default:
+          $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+          break;
+      }
+    }
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Use the form to delete the v2t1 configuration.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $edit["grants[{$this->vocabs['v2']->vid}][{$this->terms['v2t1']->tid}][remove]"] = 1;
+    $this->drupalPost(NULL, $edit, 'Delete selected');
+
+    // Log out.
+    $this->drupalLogout();
+
+    // Visit each page and verify whether access is allowed or denied.
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+
+      // Access to all pages should be denied.
+      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+    }
+  }
+
+  /**
+   * Tests adding a term configuration with children.
+   *
+   * @todo
+   *   Check that node access is updated for these as well.
+   */
+  public function testTermWithChildren() {
+    // Create some additional taxonomy terms in a hierarchy:
+    // v1
+    // - v1t1
+    // - - v1t1c1
+    // - - - v1t1c1g1
+    // - - - v1t1c1g2
+    // - - v1t1c2
+    // - - v1t2
+
+    $this->terms['v1t1c1'] = $this->createTerm(
+      'v1t1c1',
+      $this->vocabs['v1'],
+      $this->terms['v1t1']->tid
+    );
+    $this->terms['v1t1c2'] = $this->createTerm(
+      'v1t1c2',
+      $this->vocabs['v1'],
+      $this->terms['v1t1']->tid
+    );
+    $this->terms['v1t1c1g1'] = $this->createTerm(
+      'v1t1c1g1',
+      $this->vocabs['v1'],
+      $this->terms['v1t1c1']->tid
+    );
+    $this->terms['v1t1c1g2'] = $this->createTerm(
+      'v1t1c1g2',
+      $this->vocabs['v1'],
+      $this->terms['v1t1c1']->tid
+    );
+
+    // Add pages tagged with each.
+    foreach (array('v1t1c1', 'v1t1c2', 'v1t1c1g1', 'v1t1c1g2') as $name) {
+      $this->pages[$name] = $this->createPage(array($name));
+    }
+
+    // Log in as the administrator.
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Enable v1 programmatically.
+    taxonomy_access_enable_vocab($this->vocabs['v1']->vid, DRUPAL_ANONYMOUS_RID);
+    // Use the admin form to give anonymous view allow for v1t1 and children.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . '/role/' . DRUPAL_ANONYMOUS_RID . '/edit');
+    $edit = array();
+    $edit["new[{$this->vocabs['v1']->vid}][recursive]"] = 1;
+    $this->addFormRow($edit, $this->vocabs['v1']->vid, $this->terms['v1t1']->tid, TAXONOMY_ACCESS_NODE_ALLOW);
+    $this->drupalPost(NULL, $edit, 'Add');
+
+  }
+
+  /**
+   * Tests enabling and disabling TAC for a custom role.
+   */
+  public function testRoleEnableDisable() {
+    // Save some typing.
+    $rid = $this->user_roles['regular_user']->rid;
+    $name = $this->user_roles['regular_user']->name;
+
+    // Check that the role is disabled by default.
+    $this->checkRoleConfig(array(
+      DRUPAL_ANONYMOUS_RID => TRUE,
+      DRUPAL_AUTHENTICATED_RID => TRUE,
+      $rid => FALSE,
+    ));
+
+    // Test enabling the role.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit");
+
+    // Check that there is:
+    // - An enable link
+    // - No disable link
+    // @todo
+    //   - No grant tables.
+    $this->checkRoleEnableLink($rid, TRUE);
+    $this->checkRoleDisableLink($rid, FALSE);
+
+    // Enable the role and check that there is:
+    // - A disable link
+    // - No enable link
+    // @todo
+    //   - A global default table (with correct values?)
+    //   - An "Add vocabulary" fieldset.
+    //   - No vocabulary fieldsets or term data.
+    $this->clickLink(format_string('Enable @name', array('@name' => $name)));
+    $this->checkRoleEnableLink($rid, FALSE);
+    $this->checkRoleDisableLink($rid, TRUE);
+
+    // Update the global default to allow view.
+    $edit = array();
+    $this->configureFormRow($edit, TAXONOMY_ACCESS_GLOBAL_DEFAULT, TAXONOMY_ACCESS_VOCABULARY_DEFAULT, TAXONOMY_ACCESS_NODE_ALLOW);
+    $this->drupalPost(NULL, $edit, 'Save all');
+
+    // Confirm that all three roles are enabled.
+    $this->checkRoleConfig(array(
+      DRUPAL_ANONYMOUS_RID => TRUE,
+      DRUPAL_AUTHENTICATED_RID => TRUE,
+      $rid => TRUE,
+    ));
+
+    // Check that the role is configured.
+    $r =
+      db_query(
+        'SELECT grant_view FROM {taxonomy_access_default}
+         WHERE vid = :vid AND rid = :rid',
+        array(':vid' => TAXONOMY_ACCESS_GLOBAL_DEFAULT, ':rid' => $rid)
+      )
+      ->fetchField();
+    $this->assertTrue($r == TAXONOMY_ACCESS_NODE_ALLOW, t('Used form to grant the role %role view in the global default.', array('%role' => $name)));
+
+    // Log in as the regular_user.
+    $this->drupalLogout();
+    $this->drupalLogin($this->users['regular_user']);
+
+    // Visit each node and verify that access is allowed.
+    foreach ($this->articles as $key => $article) {
+      $this->drupalGet('node/' . $article->nid);
+      $this->assertResponse(200, t("Access to %name article (nid %nid) is allowed.", array('%name' => $key, '%nid' => $article->nid)));
+    }
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+      $this->assertResponse(200, t("Access to %name page (nid %nid) is allowed.", array('%name' => $key, '%nid' => $page->nid)));
+    }
+
+    // Log in as the administrator.
+    $this->drupalLogout();
+    $this->drupalLogin($this->users['site_admin']);
+
+    // Test disabling the role.
+    $this->drupalGet(TAXONOMY_ACCESS_CONFIG . "/role/$rid/edit");
+    $this->clickLink(t('Disable @name', array('@name' => $name)));
+    $this->assertText("Are you sure you want to delete all taxonomy access rules for the role $name", t('Disable form for role loaded.'));
+    $this->drupalPost(NULL, array(), 'Delete all');
+
+    // Confirm that a confirmation message appears.
+    $this->assertText("All taxonomy access rules deleted for role $name", t('Confirmation message found.'));
+
+    // Check that there is:
+    // - An enable link
+    // - No disable link
+    // @todo
+    //   - No grant tables.
+    $this->checkRoleEnableLink($rid, TRUE);
+    $this->checkRoleDisableLink($rid, FALSE);
+
+    // Confirm edit/enable/disable links are in their original state.
+    $this->checkRoleConfig(array(
+      DRUPAL_ANONYMOUS_RID => TRUE,
+      DRUPAL_AUTHENTICATED_RID => TRUE,
+      $rid => FALSE,
+    ));
+
+    // Check that the role is no longer configured.
+    $r =
+      db_query(
+        'SELECT grant_view FROM {taxonomy_access_default}
+         WHERE rid = :rid',
+        array(':rid' => $rid)
+      )
+      ->fetchAll();
+    $this->assertTrue(empty($r), t('All records removed for role %role.', array('%role' => $name)));
+
+    // @todo
+    //   - Add a term configuration and make sure that gets deleted too.
+
+    // Log in as the regular_user.
+    $this->drupalLogout();
+    $this->drupalLogin($this->users['regular_user']);
+
+    // Visit all nodes and verify that access is again denied.
+    foreach ($this->articles as $key => $article) {
+      $this->drupalGet('node/' . $article->nid);
+      $this->assertResponse(403, t("Access to %name article (nid %nid) is denied.", array('%name' => $key, '%nid' => $article->nid)));
+    }
+    foreach ($this->pages as $key => $page) {
+      $this->drupalGet('node/' . $page->nid);
+      $this->assertResponse(403, t("Access to %name page (nid %nid) is denied.", array('%name' => $key, '%nid' => $page->nid)));
+    }
+  }
+}
+
+/**
+ * Tests node access for all possible grant combinations.
+ */
+class TaxonomyAccessNodeGrantTest extends TaxonomyAccessTestCase {
+
+  // There are three roles for node access testing:
+  // global_allow   Receives "Allow" in the global default.
+  // global_ignore  Receives "Ignore" in the global default.
+  // global_deny    Receives "Deny" in the global default.
+  // All roles receive the same permissions for terms and vocab defaults.
+  protected $roles = array();
+  protected $role_config = array(
+    'global_allow' => array(),
+    'global_ignore' => array(),
+    'global_deny' => array(),
+  );
+
+  protected $vocabs = array();
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Node access',
+      'description' => 'Test node access for various grant configurations.',
+      'group' => 'Taxonomy Access Control',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    // Configure roles with no additional permissions.
+    foreach ($this->role_config as $role_name => $permissions) {
+      $this->roles[$role_name] = $this->drupalCreateRole(array(), $role_name);
+    }
+
+    $node_grants = array('view', 'update', 'delete');
+
+    // Set up our testing taxonomy.
+
+    // We will create 4 vocabularies: a, i, d, and nc
+    // These names indicate what grant the vocab. default will have for view.
+    // (NC means the vocab default is not configured.)
+
+    $grant_types = array(
+      'a' => array(),
+      'i' => array(),
+      'd' => array(),
+      'nc' => array(),
+    );
+
+    // View alone can be used to test V/U/D because the logic is identical.
+    foreach ($node_grants as $grant) {
+      $grant_types['a'][$grant] = TAXONOMY_ACCESS_NODE_ALLOW;
+      $grant_types['i'][$grant] = TAXONOMY_ACCESS_NODE_IGNORE;
+      $grant_types['d'][$grant] = TAXONOMY_ACCESS_NODE_DENY;
+    }
+
+    // Each vocabulary will have four parent terms in the same fashion:
+    // a_parent, i_parent, d_parent, and nc_parent.
+
+    // Each of these_parent terms will have children in each class, as well:
+    // a_child, i_child, d_child, and nc_child.
+
+    // So, each vocab looks something like:
+    // - a_parent
+    // - - a_child
+    // - - i_child
+    // - - d_child
+    // - - nc_child
+    // - i_parent
+    // - - a_child
+    // - - i_child
+    // - - d_child
+    // - - nc_child
+    // - d_parent
+    // - - a_child
+    // - - i_child
+    // - - d_child
+    // - - nc_child
+    // - nc_parent
+    // - - a_child
+    // - - i_child
+    // - - d_child
+    // - - nc_child
+
+    $term_rows = array();
+    $default_rows = array();
+    $this->setUpAssertions = array();
+
+    // Configure terms, vocabularies, and grants.
+    foreach ($grant_types as $vocab_name => $default_grants) {
+      // Create the vocabulary.
+      $vocab_name = "v" . $vocab_name;
+      $this->vocabs[$vocab_name] = array();
+      $this->vocabs[$vocab_name]['vocab'] = parent::createVocab($vocab_name);
+      $this->vocabs[$vocab_name]['terms'] = array();
+      $vocab = $this->vocabs[$vocab_name]['vocab'];
+
+      // Add a field for the vocabulary to pages.
+      $this->createField($vocab_name);
+
+      // Configure default grants for the vocabulary for each role.
+      if (!empty($default_grants)) {
+        foreach ($this->roles as $name => $role) {
+          $default_rows[] =  _taxonomy_access_format_grant_record($vocab->vid, $role, $default_grants, TRUE);
+          $this->setUpAssertions[] = array(
+            'grant' => $default_grants['view'],
+            'query' => 'SELECT grant_view FROM {taxonomy_access_default} WHERE vid = :vid AND rid = :rid',
+            'args' => array(':vid' => $vocab->vid, ':rid' => $role),
+            'message' => t('Configured default grants for vocab %vocab, role %role', array('%vocab' => $vocab->machine_name, '%role' => $name)),
+          );
+        }
+      }
+
+      // Create terms.
+      foreach ($grant_types as $parent_name => $parent_grants) {
+
+        // Create parent term.
+        $parent_name = $vocab_name . "__" . $parent_name . "_parent";
+        $this->vocabs[$vocab_name]['terms'][$parent_name] =
+          parent::createTerm($parent_name, $vocab);
+        $parent_id = $this->vocabs[$vocab_name]['terms'][$parent_name]->tid;
+
+        // Configure grants for the parent term for each role.
+        if (!empty($parent_grants)) {
+          foreach ($this->roles as $name => $role) {
+            $term_rows[] =  _taxonomy_access_format_grant_record($parent_id, $role, $parent_grants);
+            $this->setUpAssertions[] = array(
+              'grant' => $parent_grants['view'],
+              'query' => 'SELECT grant_view FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
+              'args' => array(':tid' => $parent_id, ':rid' => $role),
+              'message' => t('Configured grants for term %term, role %role', array('%term' => $parent_name, '%role' => $name)),
+            );
+          }
+        }
+
+        // Create child terms.
+        foreach ($grant_types as $child_name => $child_grants) {
+          $child_name = $parent_name . "__" . $child_name . "_child";
+          $this->vocabs[$vocab_name]['terms'][$child_name] =
+            parent::createTerm($child_name, $vocab, $parent_id);
+          $child_id = $this->vocabs[$vocab_name]['terms'][$child_name]->tid;
+
+          // Configure grants for the child term for each role.
+          if (!empty($child_grants)) {
+            foreach ($this->roles as $name => $role) {
+              $term_rows[] =  _taxonomy_access_format_grant_record($child_id, $role, $child_grants);
+              $this->setUpAssertions[] = array(
+                'grant' => $child_grants['view'],
+                'query' => 'SELECT grant_view FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
+                'args' => array(':tid' => $child_id, ':rid' => $role),
+                'message' => t('Configured grants for term %term, role %role', array('%term' => $child_name, '%role' => $name)),
+              );
+            }
+          }
+        }
+      }
+    }
+
+    // Set the grants.
+    taxonomy_access_set_default_grants($default_rows);
+    taxonomy_access_set_term_grants($term_rows);
+  }
+
+  /**
+   * Verifies that all grants were properly stored during setup.
+   */
+  public function testSetUpCheck() {
+    // Check that all records were properly stored.
+    foreach ($this->setUpAssertions as $assertion) {
+      $r = db_query($assertion['query'], $assertion['args'])->fetchField();
+      $this->assertTrue(
+        (is_numeric($r) && $r == $assertion['grant']),
+        $assertion['message']
+      );
+    }
+  }
+
+  // Role config tests:
+  // Create a role
+  // Create a user with the role
+  // Configure role grants via form
+  // Add, with children, delete
+  // Confirm records stored
+  // Confirm node access properly updated
+  // Go back and edit, repeat.
+  // Disable role.
+  // Confirm form.
+  // Update node access if prompted.
+  // Confirm records deleted.
+  // Confirm node access updated.
+
+  // 1. delete a term
+  // 2. change a grant config
+  // 3. delete a grant config
+  // 4. change a vocab default
+  // 5. delete a voacb default
+  // 6. disable a role
+  // 7. delete a role
+  // 8. delete a field attachment
+  // 9. delete a vocabulary
+}
+
+/**
+ * Tests term grants for all possible grant combinations.
+ */
+class TaxonomyAccessTermGrantTest extends TaxonomyAccessTestCase {
+  // There are four roles for term access testing:
+  // ctlt   Receives both "Create" and "List" in the global default.
+  // ctlf   Receives "Create" but not "List" in the global default.
+  // cflt   Receives "List" but not "Create" in the global default.
+  // cflf   Receives neither "Create" nor "List" in the global default.
+  // All roles receive the same permissions for terms and vocab defaults.
+  protected $roles = array();
+  protected $role_config = array(
+    'ctlt' => array(),
+    'ctlf' => array(),
+    'cflt' => array(),
+    'cflf' => array(),
+  );
+
+  protected $vocabs = array();
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Term grants',
+      'description' => 'Test node access for View tag (create) and Add tag (list) grants.',
+      'group' => 'Taxonomy Access Control',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp();
+
+    // Configure roles with no additional permissions.
+    foreach ($this->role_config as $role_name => $permissions) {
+      $this->roles[$role_name] = $this->drupalCreateRole(array(), $role_name);
+    }
+
+    // Set up our testing taxonomy.
+
+    // We will create four vocabularies:
+    // vctlt   Receives both "Create" and "List" in the vocabulary default.
+    // vctlf   Receives "Create" but not "List" in the vocabulary default.
+    // vcflt   Receives "List" but not "Create" in the vocabulary default.
+    // vcflf   Receives neither "Create" nor "List" in the vocabulary default.
+    $grant_combos = array(
+      'ctlt' => array('create' => TAXONOMY_ACCESS_TERM_ALLOW, 'list' => TAXONOMY_ACCESS_TERM_ALLOW),
+      'ctlf' => array('create' => TAXONOMY_ACCESS_TERM_ALLOW, 'list' => TAXONOMY_ACCESS_TERM_DENY),
+      'cflt' => array('create' => TAXONOMY_ACCESS_TERM_DENY, 'list' => TAXONOMY_ACCESS_TERM_ALLOW),
+      'cflf' => array('create' => TAXONOMY_ACCESS_TERM_DENY, 'list' => TAXONOMY_ACCESS_TERM_DENY),
+    );
+
+    // Grant all rows view, update, and delete.
+    foreach ($grant_combos as $combo) {
+      $combo['view'] = TAXONOMY_ACCESS_NODE_ALLOW;
+      $combo['update'] = TAXONOMY_ACCESS_NODE_ALLOW;
+      $combo['delete'] = TAXONOMY_ACCESS_NODE_ALLOW;
+    }
+
+    // Each vocabulary will have four parent terms in the same fashion:
+    // ctlt_parent, ctlf_parent, cflt_parent, and cflf_parent.
+
+    // Each of these_parent terms will have children in each class, as well:
+    // ctlt_child, ctlf_child, cflt_child, and cflf_child.
+
+    // So, each vocab looks something like:
+    // - ctlt_parent
+    // - - ctlt_child
+    // - - ctlf_child
+    // - - cflt_child
+    // - - cflf_child
+    // - ctlf_parent
+    // - - ctlt_child
+    // - - ctlf_child
+    // - - cflt_child
+    // - - cfl_fchild
+    // - cflt_parent
+    // - - ctlt_child
+    // - - ctlf_child
+    // - - cflt_child
+    // - - cflf_child
+    // - cflf_parent
+    // - - ctlt_child
+    // - - ctlf_child
+    // - - cflt_child
+    // - - cflf_child
+
+    // Configure terms, vocabularies, and grants.
+    foreach ($grant_combos as $vocab_name => $default_grants) {
+      // Create the vocabulary.
+      $vocab_name = "v" . $vocab_name;
+      $this->vocabs[$vocab_name] = array();
+      $this->vocabs[$vocab_name]['vocab'] = parent::createVocab($vocab_name);
+      $this->vocabs[$vocab_name]['terms'] = array();
+      $vocab = $this->vocabs[$vocab_name]['vocab'];
+
+      // Add a field for the vocabulary to pages.
+      $this->createField($vocab_name);
+
+      // Configure default grants for the vocabulary for each role.
+      if (!empty($default_grants)) {
+        foreach ($this->roles as $name => $role) {
+          $default_rows[] =  _taxonomy_access_format_grant_record($vocab->vid, $role, $default_grants, TRUE);
+          $this->setUpAssertions[] = array(
+            'create' => $default_grants['create'],
+            'list' => $default_grants['list'],
+            'query' => 'SELECT grant_create, grant_list FROM {taxonomy_access_default} WHERE vid = :vid AND rid = :rid',
+            'args' => array(':vid' => $vocab->vid, ':rid' => $role),
+            'message' => t('Configured default grants for vocab %vocab, role %role', array('%vocab' => $vocab->machine_name, '%role' => $name)),
+          );
+        }
+      }
+
+      // Create terms.
+      foreach ($grant_combos as $parent_name => $parent_grants) {
+
+        // Create parent term.
+        $parent_name = $vocab_name . "__" . $parent_name . "_parent";
+        $this->vocabs[$vocab_name]['terms'][$parent_name] =
+          parent::createTerm($parent_name, $vocab);
+        $parent_id = $this->vocabs[$vocab_name]['terms'][$parent_name]->tid;
+
+        // Configure grants for the parent term for each role.
+        if (!empty($parent_grants)) {
+          foreach ($this->roles as $name => $role) {
+            $term_rows[] =  _taxonomy_access_format_grant_record($parent_id, $role, $parent_grants);
+            $this->setUpAssertions[] = array(
+              'create' => $parent_grants['create'],
+              'list' => $parent_grants['list'],
+              'query' => 'SELECT grant_create, grant_list FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
+              'args' => array(':tid' => $parent_id, ':rid' => $role),
+              'message' => t('Configured grants for term %term, role %role', array('%term' => $parent_name, '%role' => $name)),
+            );
+          }
+        }
+
+        // Create child terms.
+        foreach ($grant_combos as $child_name => $child_grants) {
+          $child_name = $parent_name . "__" . $child_name . "_child";
+          $this->vocabs[$vocab_name]['terms'][$child_name] =
+            parent::createTerm($child_name, $vocab, $parent_id);
+          $child_id = $this->vocabs[$vocab_name]['terms'][$child_name]->tid;
+
+          // Configure grants for the child term for each role.
+          if (!empty($child_grants)) {
+            foreach ($this->roles as $name => $role) {
+              $term_rows[] =  _taxonomy_access_format_grant_record($child_id, $role, $child_grants);
+              $this->setUpAssertions[] = array(
+                'create' => $child_grants['create'],
+                'list' => $child_grants['list'],
+                'query' => 'SELECT grant_create, grant_list FROM {taxonomy_access_term} WHERE tid = :tid AND rid = :rid',
+                'args' => array(':tid' => $child_id, ':rid' => $role),
+                'message' => t('Configured grants for term %term, role %role', array('%term' => $child_name, '%role' => $name)),
+              );
+            }
+          }
+        }
+      }
+    }
+
+    // Set the grants.
+    taxonomy_access_set_default_grants($default_rows);
+    taxonomy_access_set_term_grants($term_rows);
+  }
+
+  /**
+   * Verifies that all grants were properly stored during setup.
+   */
+  public function testSetUpCheck() {
+    // Check that all records were properly stored.
+    foreach ($this->setUpAssertions as $assertion) {
+      $r = db_query($assertion['query'], $assertion['args'])->fetchAssoc();
+      $this->assertTrue(
+        (is_array($r)
+          && $r['grant_create'] == $assertion['create']
+          && $r['grant_list'] == $assertion['list']),
+        $assertion['message']
+      );
+    }
+  }
+}
+
+class TaxonomyAccessWeightTest extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Weight',
+      'description' => 'Test module weight.',
+      'group' => 'Taxonomy Access Control',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp('taxonomy_access');
+  }
+
+  /**
+  * Verifies that this module is weighted below the Taxonomy module.
+  */
+  public function testWeight() {
+
+    // Verify weight.
+    $tax_weight =
+      db_query(
+        "SELECT weight FROM {system}
+         WHERE name = 'taxonomy'")
+      ->fetchField();
+    $tax_access_weight =
+      db_query(
+        "SELECT weight FROM {system}
+         WHERE name = 'taxonomy_access'")
+      ->fetchField();
+    $this->assertTrue(
+      $tax_access_weight > $tax_weight,
+      t("Weight of this module is @tax_access_weight. Weight of the Taxonomy module is @tax_weight.",
+      array('@tax_access_weight' => $tax_access_weight, '@tax_weight' => $tax_weight))
+    );
+
+    // Disable module and set weight of the Taxonomy module to a high number.
+    module_disable(array('taxonomy_access'), TRUE);
+    db_update('system')
+    ->fields(array('weight' => rand(5000, 9000)))
+    ->condition('name', 'taxonomy')
+    ->execute();
+
+    // Re-enable module and re-verify weight.
+    module_enable(array('taxonomy_access'), TRUE);
+    $tax_weight =
+      db_query(
+        "SELECT weight FROM {system}
+         WHERE name = 'taxonomy'")
+      ->fetchField();
+    $tax_access_weight =
+      db_query(
+        "SELECT weight FROM {system}
+         WHERE name = 'taxonomy_access'")
+      ->fetchField();
+    $this->assertTrue(
+      $tax_access_weight > $tax_weight,
+      t("Weight of this module is @tax_access_weight. Weight of the Taxonomy module is @tax_weight.",
+      array('@tax_access_weight' => $tax_access_weight, '@tax_weight' => $tax_weight))
+    );
+  }
+}
+
+
+/**
+ * Tests that callbacks are cleaned up when the module is disabled.
+ */
+class TaxonomyAccessCallbackCleanupTest extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Callback Cleanup',
+      'description' => 'Test callback cleanup during disabling of module works.',
+      'group' => 'Taxonomy Access Control',
+    );
+  }
+
+  public function setUp() {
+    parent::setUp('taxonomy_access');
+  }
+
+  /**
+   * Verifies that the module's callbacks are cleaned up during disable.
+   */
+  public function testCallbackCleanup() {
+
+    // The problem only happens on new fields after the module is installed.
+    $content_type = $this->drupalCreateContentType();
+
+    // Create a new field with type taxonomy_term_reference.
+    $field_name = drupal_strtolower($this->randomName() . '_field_name');
+    $field_type = array(
+      'field_name' => $field_name,
+      'type' => 'taxonomy_term_reference',
+      'cardinality' => 1,
+    );
+    $field_type = field_create_field($field_type);
+
+    // Add an instance of the field to content type.
+    $field_instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'bundle' => $content_type->name
+    );
+    $field_instance = field_create_instance($field_instance);
+
+    // Trigger hook_disable to see if the callbacks are cleaned up.
+    module_disable(array('taxonomy_access'), TRUE);
+
+    // Create a user so that we can check if we can access the node add pages.
+    $this->privileged_user = $this->drupalCreateUser(array('bypass node access'));
+    $this->drupalLogin($this->privileged_user);
+
+    // If the callbacks are not cleaned up we would get a fatal error.
+    $this->drupalGet('node/add/' . $content_type->name);
+    $this->assertText(t('Create @name', array('@name' => $content_type->name)), t('New content can be added'));
+  }
+}

+ 66 - 0
sites/all/modules/features/showroom/showroom.features.field_base.inc

@@ -31,5 +31,71 @@ function showroom_field_default_field_bases() {
     'type' => 'text_with_summary',
   );
 
+  // Exported field_base: 'field_showroom'.
+  $field_bases['field_showroom'] = array(
+    'active' => 1,
+    'cardinality' => 1,
+    'deleted' => 0,
+    'entity_types' => array(),
+    'field_name' => 'field_showroom',
+    'field_permissions' => array(
+      'type' => 2,
+    ),
+    'indexes' => array(
+      'tid' => array(
+        0 => 'tid',
+      ),
+    ),
+    'locked' => 0,
+    'module' => 'taxonomy',
+    'settings' => array(
+      'allowed_values' => array(
+        0 => array(
+          'vocabulary' => 'showroom',
+          'parent' => 0,
+          'depth' => '',
+        ),
+      ),
+      'entity_translation_sync' => FALSE,
+      'options_list_callback' => 'content_taxonomy_allowed_values',
+      'profile2_private' => FALSE,
+    ),
+    'translatable' => 0,
+    'type' => 'taxonomy_term_reference',
+  );
+
+  // Exported field_base: 'field_tode_showroom'.
+  $field_bases['field_tode_showroom'] = array(
+    'active' => 1,
+    'cardinality' => 1,
+    'deleted' => 0,
+    'entity_types' => array(),
+    'field_name' => 'field_tode_showroom',
+    'field_permissions' => array(
+      'type' => 2,
+    ),
+    'indexes' => array(
+      'tid' => array(
+        0 => 'tid',
+      ),
+    ),
+    'locked' => 0,
+    'module' => 'taxonomy',
+    'settings' => array(
+      'allowed_values' => array(
+        0 => array(
+          'vocabulary' => 'showroom',
+          'parent' => 0,
+          'depth' => '',
+        ),
+      ),
+      'entity_translation_sync' => FALSE,
+      'options_list_callback' => 'content_taxonomy_allowed_values',
+      'profile2_private' => FALSE,
+    ),
+    'translatable' => 0,
+    'type' => 'taxonomy_term_reference',
+  );
+
   return $field_bases;
 }

+ 937 - 0
sites/all/modules/features/showroom/showroom.features.field_instance.inc

@@ -10,6 +10,904 @@
 function showroom_field_default_field_instances() {
   $field_instances = array();
 
+  // Exported field_instance: 'node-showroom-body'.
+  $field_instances['node-showroom-body'] = array(
+    'bundle' => 'showroom',
+    'default_value' => NULL,
+    'deleted' => 0,
+    'description' => '',
+    'display' => array(
+      'bookmark' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardbig' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardfull' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardmedium' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardsmall' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'default' => array(
+        'label' => 'hidden',
+        'module' => 'text',
+        'settings' => array(),
+        'type' => 'text_default',
+        'weight' => 0,
+      ),
+      'homeblock' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'teaser' => array(
+        'label' => 'hidden',
+        'module' => 'text',
+        'settings' => array(
+          'trim_length' => 600,
+        ),
+        'type' => 'text_summary_or_trimmed',
+        'weight' => 0,
+      ),
+    ),
+    'entity_type' => 'node',
+    'field_name' => 'body',
+    'label' => 'Body',
+    'required' => FALSE,
+    'settings' => array(
+      'display_summary' => TRUE,
+      'entity_translation_sync' => FALSE,
+      'text_processing' => 1,
+      'user_register_form' => FALSE,
+    ),
+    'widget' => array(
+      'module' => 'text',
+      'settings' => array(
+        'rows' => 20,
+        'summary_rows' => 5,
+      ),
+      'type' => 'text_textarea_with_summary',
+      'weight' => 5,
+    ),
+  );
+
+  // Exported field_instance: 'node-showroom-field_public_address'.
+  $field_instances['node-showroom-field_public_address'] = array(
+    'bundle' => 'showroom',
+    'default_value' => NULL,
+    'deleted' => 0,
+    'description' => '',
+    'display' => array(
+      'bookmark' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardbig' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardfull' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardmedium' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardsmall' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'default' => array(
+        'label' => 'above',
+        'module' => 'addressfield',
+        'settings' => array(
+          'format_handlers' => array(
+            0 => 'address',
+          ),
+          'use_widget_handlers' => 1,
+        ),
+        'type' => 'addressfield_default',
+        'weight' => 2,
+      ),
+      'homeblock' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'teaser' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+    ),
+    'entity_type' => 'node',
+    'field_name' => 'field_public_address',
+    'label' => 'Adresse',
+    'required' => 0,
+    'settings' => array(
+      'entity_translation_sync' => FALSE,
+      'user_register_form' => FALSE,
+    ),
+    'widget' => array(
+      'active' => 1,
+      'module' => 'addressfield',
+      'settings' => array(
+        'available_countries' => array(
+          'AD' => 'AD',
+          'AE' => 'AE',
+          'AF' => 'AF',
+          'AG' => 'AG',
+          'AI' => 'AI',
+          'AL' => 'AL',
+          'AM' => 'AM',
+          'AN' => 'AN',
+          'AO' => 'AO',
+          'AQ' => 'AQ',
+          'AR' => 'AR',
+          'AS' => 'AS',
+          'AT' => 'AT',
+          'AU' => 'AU',
+          'AW' => 'AW',
+          'AX' => 'AX',
+          'AZ' => 'AZ',
+          'BA' => 'BA',
+          'BB' => 'BB',
+          'BD' => 'BD',
+          'BE' => 'BE',
+          'BF' => 'BF',
+          'BG' => 'BG',
+          'BH' => 'BH',
+          'BI' => 'BI',
+          'BJ' => 'BJ',
+          'BL' => 'BL',
+          'BM' => 'BM',
+          'BN' => 'BN',
+          'BO' => 'BO',
+          'BQ' => 'BQ',
+          'BR' => 'BR',
+          'BS' => 'BS',
+          'BT' => 'BT',
+          'BV' => 'BV',
+          'BW' => 'BW',
+          'BY' => 'BY',
+          'BZ' => 'BZ',
+          'CA' => 'CA',
+          'CC' => 'CC',
+          'CD' => 'CD',
+          'CF' => 'CF',
+          'CG' => 'CG',
+          'CH' => 'CH',
+          'CI' => 'CI',
+          'CK' => 'CK',
+          'CL' => 'CL',
+          'CM' => 'CM',
+          'CN' => 'CN',
+          'CO' => 'CO',
+          'CR' => 'CR',
+          'CU' => 'CU',
+          'CV' => 'CV',
+          'CW' => 'CW',
+          'CX' => 'CX',
+          'CY' => 'CY',
+          'CZ' => 'CZ',
+          'DE' => 'DE',
+          'DJ' => 'DJ',
+          'DK' => 'DK',
+          'DM' => 'DM',
+          'DO' => 'DO',
+          'DZ' => 'DZ',
+          'EC' => 'EC',
+          'EE' => 'EE',
+          'EG' => 'EG',
+          'EH' => 'EH',
+          'ER' => 'ER',
+          'ES' => 'ES',
+          'ET' => 'ET',
+          'FI' => 'FI',
+          'FJ' => 'FJ',
+          'FK' => 'FK',
+          'FM' => 'FM',
+          'FO' => 'FO',
+          'FR' => 'FR',
+          'GA' => 'GA',
+          'GB' => 'GB',
+          'GD' => 'GD',
+          'GE' => 'GE',
+          'GF' => 'GF',
+          'GG' => 'GG',
+          'GH' => 'GH',
+          'GI' => 'GI',
+          'GL' => 'GL',
+          'GM' => 'GM',
+          'GN' => 'GN',
+          'GP' => 'GP',
+          'GQ' => 'GQ',
+          'GR' => 'GR',
+          'GS' => 'GS',
+          'GT' => 'GT',
+          'GU' => 'GU',
+          'GW' => 'GW',
+          'GY' => 'GY',
+          'HK' => 'HK',
+          'HM' => 'HM',
+          'HN' => 'HN',
+          'HR' => 'HR',
+          'HT' => 'HT',
+          'HU' => 'HU',
+          'ID' => 'ID',
+          'IE' => 'IE',
+          'IL' => 'IL',
+          'IM' => 'IM',
+          'IN' => 'IN',
+          'IO' => 'IO',
+          'IQ' => 'IQ',
+          'IR' => 'IR',
+          'IS' => 'IS',
+          'IT' => 'IT',
+          'JE' => 'JE',
+          'JM' => 'JM',
+          'JO' => 'JO',
+          'JP' => 'JP',
+          'KE' => 'KE',
+          'KG' => 'KG',
+          'KH' => 'KH',
+          'KI' => 'KI',
+          'KM' => 'KM',
+          'KN' => 'KN',
+          'KP' => 'KP',
+          'KR' => 'KR',
+          'KW' => 'KW',
+          'KY' => 'KY',
+          'KZ' => 'KZ',
+          'LA' => 'LA',
+          'LB' => 'LB',
+          'LC' => 'LC',
+          'LI' => 'LI',
+          'LK' => 'LK',
+          'LR' => 'LR',
+          'LS' => 'LS',
+          'LT' => 'LT',
+          'LU' => 'LU',
+          'LV' => 'LV',
+          'LY' => 'LY',
+          'MA' => 'MA',
+          'MC' => 'MC',
+          'MD' => 'MD',
+          'ME' => 'ME',
+          'MF' => 'MF',
+          'MG' => 'MG',
+          'MH' => 'MH',
+          'MK' => 'MK',
+          'ML' => 'ML',
+          'MM' => 'MM',
+          'MN' => 'MN',
+          'MO' => 'MO',
+          'MP' => 'MP',
+          'MQ' => 'MQ',
+          'MR' => 'MR',
+          'MS' => 'MS',
+          'MT' => 'MT',
+          'MU' => 'MU',
+          'MV' => 'MV',
+          'MW' => 'MW',
+          'MX' => 'MX',
+          'MY' => 'MY',
+          'MZ' => 'MZ',
+          'NA' => 'NA',
+          'NC' => 'NC',
+          'NE' => 'NE',
+          'NF' => 'NF',
+          'NG' => 'NG',
+          'NI' => 'NI',
+          'NL' => 'NL',
+          'NO' => 'NO',
+          'NP' => 'NP',
+          'NR' => 'NR',
+          'NU' => 'NU',
+          'NZ' => 'NZ',
+          'OM' => 'OM',
+          'PA' => 'PA',
+          'PE' => 'PE',
+          'PF' => 'PF',
+          'PG' => 'PG',
+          'PH' => 'PH',
+          'PK' => 'PK',
+          'PL' => 'PL',
+          'PM' => 'PM',
+          'PN' => 'PN',
+          'PR' => 'PR',
+          'PS' => 'PS',
+          'PT' => 'PT',
+          'PW' => 'PW',
+          'PY' => 'PY',
+          'QA' => 'QA',
+          'RE' => 'RE',
+          'RO' => 'RO',
+          'RS' => 'RS',
+          'RU' => 'RU',
+          'RW' => 'RW',
+          'SA' => 'SA',
+          'SB' => 'SB',
+          'SC' => 'SC',
+          'SD' => 'SD',
+          'SE' => 'SE',
+          'SG' => 'SG',
+          'SH' => 'SH',
+          'SI' => 'SI',
+          'SJ' => 'SJ',
+          'SK' => 'SK',
+          'SL' => 'SL',
+          'SM' => 'SM',
+          'SN' => 'SN',
+          'SO' => 'SO',
+          'SR' => 'SR',
+          'SS' => 'SS',
+          'ST' => 'ST',
+          'SV' => 'SV',
+          'SX' => 'SX',
+          'SY' => 'SY',
+          'SZ' => 'SZ',
+          'TC' => 'TC',
+          'TD' => 'TD',
+          'TF' => 'TF',
+          'TG' => 'TG',
+          'TH' => 'TH',
+          'TJ' => 'TJ',
+          'TK' => 'TK',
+          'TL' => 'TL',
+          'TM' => 'TM',
+          'TN' => 'TN',
+          'TO' => 'TO',
+          'TR' => 'TR',
+          'TT' => 'TT',
+          'TV' => 'TV',
+          'TW' => 'TW',
+          'TZ' => 'TZ',
+          'UA' => 'UA',
+          'UG' => 'UG',
+          'UM' => 'UM',
+          'US' => 'US',
+          'UY' => 'UY',
+          'UZ' => 'UZ',
+          'VA' => 'VA',
+          'VC' => 'VC',
+          'VE' => 'VE',
+          'VG' => 'VG',
+          'VI' => 'VI',
+          'VN' => 'VN',
+          'VU' => 'VU',
+          'WF' => 'WF',
+          'WS' => 'WS',
+          'YE' => 'YE',
+          'YT' => 'YT',
+          'ZA' => 'ZA',
+          'ZM' => 'ZM',
+          'ZW' => 'ZW',
+        ),
+        'default_country' => '',
+        'format_handlers' => array(
+          'address' => 'address',
+          'address-hide-postal-code' => 0,
+          'address-hide-street' => 0,
+          'address-hide-country' => 0,
+          'organisation' => 0,
+          'name-full' => 0,
+          'name-oneline' => 0,
+          'address-optional' => 0,
+        ),
+      ),
+      'type' => 'addressfield_standard',
+      'weight' => 8,
+    ),
+  );
+
+  // Exported field_instance: 'node-showroom-field_public_email'.
+  $field_instances['node-showroom-field_public_email'] = array(
+    'bundle' => 'showroom',
+    'default_value' => NULL,
+    'deleted' => 0,
+    'description' => '',
+    'display' => array(
+      'bookmark' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardbig' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardfull' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardmedium' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardsmall' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'default' => array(
+        'label' => 'above',
+        'module' => 'email',
+        'settings' => array(),
+        'type' => 'email_default',
+        'weight' => 3,
+      ),
+      'homeblock' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'teaser' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+    ),
+    'entity_type' => 'node',
+    'field_name' => 'field_public_email',
+    'label' => 'Email',
+    'required' => 0,
+    'settings' => array(
+      'entity_translation_sync' => FALSE,
+      'user_register_form' => FALSE,
+    ),
+    'widget' => array(
+      'active' => 1,
+      'module' => 'email',
+      'settings' => array(
+        'size' => 60,
+      ),
+      'type' => 'email_textfield',
+      'weight' => 10,
+    ),
+  );
+
+  // Exported field_instance: 'node-showroom-field_public_phone'.
+  $field_instances['node-showroom-field_public_phone'] = array(
+    'bundle' => 'showroom',
+    'deleted' => 0,
+    'description' => '',
+    'display' => array(
+      'bookmark' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardbig' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardfull' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardmedium' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardsmall' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'default' => array(
+        'label' => 'above',
+        'module' => 'cck_phone',
+        'settings' => array(),
+        'type' => 'global_phone_number',
+        'weight' => 4,
+      ),
+      'homeblock' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'teaser' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+    ),
+    'entity_type' => 'node',
+    'field_name' => 'field_public_phone',
+    'label' => 'Phone',
+    'required' => 0,
+    'settings' => array(
+      'all_country_codes' => 1,
+      'country_code_position' => 'after',
+      'country_codes' => array(
+        'country_selection' => array(
+          'ad' => 0,
+          'ae' => 0,
+          'af' => 0,
+          'ag' => 0,
+          'ai' => 0,
+          'al' => 0,
+          'am' => 0,
+          'an' => 0,
+          'ao' => 0,
+          'ar' => 0,
+          'as' => 0,
+          'at' => 0,
+          'au' => 0,
+          'aw' => 0,
+          'az' => 0,
+          'ba' => 0,
+          'bb' => 0,
+          'bd' => 0,
+          'be' => 0,
+          'bf' => 0,
+          'bg' => 0,
+          'bh' => 0,
+          'bi' => 0,
+          'bj' => 0,
+          'bm' => 0,
+          'bn' => 0,
+          'bo' => 0,
+          'br' => 0,
+          'bs' => 0,
+          'bt' => 0,
+          'bw' => 0,
+          'by' => 0,
+          'bz' => 0,
+          'ca' => 0,
+          'cc' => 0,
+          'cd' => 0,
+          'cf' => 0,
+          'cg' => 0,
+          'ch' => 0,
+          'ci' => 0,
+          'ck' => 0,
+          'cl' => 0,
+          'cm' => 0,
+          'cn' => 0,
+          'co' => 0,
+          'cr' => 0,
+          'cu' => 0,
+          'cv' => 0,
+          'cx' => 0,
+          'cy' => 0,
+          'cz' => 0,
+          'de' => 0,
+          'dj' => 0,
+          'dk' => 0,
+          'dm' => 0,
+          'do' => 0,
+          'dz' => 0,
+          'ec' => 0,
+          'ee' => 0,
+          'eg' => 0,
+          'er' => 0,
+          'es' => 0,
+          'et' => 0,
+          'fi' => 0,
+          'fj' => 0,
+          'fk' => 0,
+          'fm' => 0,
+          'fo' => 0,
+          'fr' => 0,
+          'ga' => 0,
+          'gb' => 0,
+          'gd' => 0,
+          'ge' => 0,
+          'gf' => 0,
+          'gh' => 0,
+          'gi' => 0,
+          'gl' => 0,
+          'gm' => 0,
+          'gn' => 0,
+          'gp' => 0,
+          'gq' => 0,
+          'gr' => 0,
+          'gt' => 0,
+          'gu' => 0,
+          'gw' => 0,
+          'gy' => 0,
+          'hk' => 0,
+          'hn' => 0,
+          'hr' => 0,
+          'ht' => 0,
+          'hu' => 0,
+          'id' => 0,
+          'ie' => 0,
+          'il' => 0,
+          'in' => 0,
+          'io' => 0,
+          'iq' => 0,
+          'ir' => 0,
+          'is' => 0,
+          'it' => 0,
+          'jm' => 0,
+          'jo' => 0,
+          'jp' => 0,
+          'ke' => 0,
+          'kg' => 0,
+          'kh' => 0,
+          'ki' => 0,
+          'km' => 0,
+          'kn' => 0,
+          'kp' => 0,
+          'kr' => 0,
+          'kw' => 0,
+          'ky' => 0,
+          'kz' => 0,
+          'la' => 0,
+          'lb' => 0,
+          'lc' => 0,
+          'li' => 0,
+          'lk' => 0,
+          'lr' => 0,
+          'ls' => 0,
+          'lt' => 0,
+          'lu' => 0,
+          'lv' => 0,
+          'ly' => 0,
+          'ma' => 0,
+          'mc' => 0,
+          'md' => 0,
+          'me' => 0,
+          'mg' => 0,
+          'mh' => 0,
+          'mk' => 0,
+          'ml' => 0,
+          'mm' => 0,
+          'mn' => 0,
+          'mo' => 0,
+          'mp' => 0,
+          'mq' => 0,
+          'mr' => 0,
+          'ms' => 0,
+          'mt' => 0,
+          'mu' => 0,
+          'mv' => 0,
+          'mw' => 0,
+          'mx' => 0,
+          'my' => 0,
+          'mz' => 0,
+          'na' => 0,
+          'nc' => 0,
+          'ne' => 0,
+          'nf' => 0,
+          'ng' => 0,
+          'ni' => 0,
+          'nl' => 0,
+          'no' => 0,
+          'np' => 0,
+          'nr' => 0,
+          'nu' => 0,
+          'nz' => 0,
+          'om' => 0,
+          'pa' => 0,
+          'pe' => 0,
+          'pf' => 0,
+          'pg' => 0,
+          'ph' => 0,
+          'pk' => 0,
+          'pl' => 0,
+          'pm' => 0,
+          'pr' => 0,
+          'ps' => 0,
+          'pt' => 0,
+          'pw' => 0,
+          'py' => 0,
+          'qa' => 0,
+          'ro' => 0,
+          'rs' => 0,
+          'ru' => 0,
+          'rw' => 0,
+          'sa' => 0,
+          'sb' => 0,
+          'sc' => 0,
+          'sd' => 0,
+          'se' => 0,
+          'sg' => 0,
+          'sh' => 0,
+          'si' => 0,
+          'sk' => 0,
+          'sl' => 0,
+          'sm' => 0,
+          'sn' => 0,
+          'so' => 0,
+          'sr' => 0,
+          'ss' => 0,
+          'st' => 0,
+          'sv' => 0,
+          'sy' => 0,
+          'sz' => 0,
+          'tc' => 0,
+          'td' => 0,
+          'tg' => 0,
+          'th' => 0,
+          'tj' => 0,
+          'tk' => 0,
+          'tm' => 0,
+          'tn' => 0,
+          'to' => 0,
+          'tp' => 0,
+          'tr' => 0,
+          'tt' => 0,
+          'tv' => 0,
+          'tw' => 0,
+          'tz' => 0,
+          'ua' => 0,
+          'ug' => 0,
+          'us' => 0,
+          'uy' => 0,
+          'uz' => 0,
+          'va' => 0,
+          'vc' => 0,
+          've' => 0,
+          'vg' => 0,
+          'vi' => 0,
+          'vn' => 0,
+          'vu' => 0,
+          'wf' => 0,
+          'ws' => 0,
+          'ye' => 0,
+          'yt' => 0,
+          'za' => 0,
+          'zm' => 0,
+          'zw' => 0,
+        ),
+        'hide_single_cc' => 0,
+      ),
+      'default_country' => 'af',
+      'enable_country_level_validation' => 1,
+      'enable_default_country' => 0,
+      'enable_extension' => 0,
+      'entity_translation_sync' => FALSE,
+      'user_register_form' => FALSE,
+    ),
+    'widget' => array(
+      'active' => 0,
+      'module' => 'cck_phone',
+      'settings' => array(
+        'size' => 15,
+      ),
+      'type' => 'phone_number',
+      'weight' => 12,
+    ),
+  );
+
+  // Exported field_instance: 'node-showroom-field_tode_showroom'.
+  $field_instances['node-showroom-field_tode_showroom'] = array(
+    'bundle' => 'showroom',
+    'deleted' => 0,
+    'description' => '',
+    'display' => array(
+      'bookmark' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardbig' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardfull' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardmedium' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'cardsmall' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'default' => array(
+        'label' => 'above',
+        'module' => 'taxonomy',
+        'settings' => array(),
+        'type' => 'taxonomy_term_reference_link',
+        'weight' => 1,
+      ),
+      'homeblock' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+      'teaser' => array(
+        'label' => 'above',
+        'settings' => array(),
+        'type' => 'hidden',
+        'weight' => 0,
+      ),
+    ),
+    'entity_type' => 'node',
+    'field_name' => 'field_tode_showroom',
+    'label' => 'Showroom',
+    'required' => 0,
+    'settings' => array(
+      'entity_translation_sync' => FALSE,
+      'user_register_form' => FALSE,
+    ),
+    'widget' => array(
+      'active' => 1,
+      'module' => 'tode',
+      'settings' => array(
+        'choose_term_parent' => 0,
+        'maxlength' => 255,
+        'redirect_node_to_term' => 0,
+        'redirect_term_to_node' => 1,
+        'show_create_tode' => 0,
+        'show_term_form' => 0,
+        'size' => 60,
+      ),
+      'type' => 'tode',
+      'weight' => 1,
+    ),
+  );
+
   // Exported field_instance: 'taxonomy_term-showroom-description_field'.
   $field_instances['taxonomy_term-showroom-description_field'] = array(
     'bundle' => 'showroom',
@@ -86,10 +984,49 @@ function showroom_field_default_field_instances() {
     ),
   );
 
+  // Exported field_instance: 'user-user-field_showroom'.
+  $field_instances['user-user-field_showroom'] = array(
+    'bundle' => 'user',
+    'default_value' => NULL,
+    'deleted' => 0,
+    'description' => '',
+    'display' => array(
+      'default' => array(
+        'label' => 'above',
+        'module' => 'taxonomy',
+        'settings' => array(),
+        'type' => 'taxonomy_term_reference_link',
+        'weight' => 2,
+      ),
+    ),
+    'entity_type' => 'user',
+    'field_name' => 'field_showroom',
+    'label' => 'Showroom',
+    'required' => 0,
+    'settings' => array(
+      'entity_translation_sync' => FALSE,
+      'user_register_form' => 1,
+    ),
+    'widget' => array(
+      'active' => 1,
+      'module' => 'options',
+      'settings' => array(
+        'content_taxonomy_opt_groups' => 0,
+      ),
+      'type' => 'options_select',
+      'weight' => 3,
+    ),
+  );
+
   // Translatables
   // Included for use with string extractors like potx.
+  t('Adresse');
+  t('Body');
   t('Description');
+  t('Email');
   t('Nom');
+  t('Phone');
+  t('Showroom');
 
   return $field_instances;
 }

+ 18 - 0
sites/all/modules/features/showroom/showroom.features.inc

@@ -12,3 +12,21 @@ function showroom_ctools_plugin_api($module = NULL, $api = NULL) {
     return array("version" => "1");
   }
 }
+
+/**
+ * Implements hook_node_info().
+ */
+function showroom_node_info() {
+  $items = array(
+    'showroom' => array(
+      'name' => t('Showroom'),
+      'base' => 'node_content',
+      'description' => '',
+      'has_title' => '1',
+      'title_label' => t('Name'),
+      'help' => '',
+    ),
+  );
+  drupal_alter('node_info', $items);
+  return $items;
+}

+ 238 - 2
sites/all/modules/features/showroom/showroom.features.user_permission.inc

@@ -10,20 +10,166 @@
 function showroom_user_default_permissions() {
   $permissions = array();
 
+  // Exported permission: 'assign Showroom role'.
+  $permissions['assign Showroom role'] = array(
+    'name' => 'assign Showroom role',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'role_delegation',
+  );
+
+  // Exported permission: 'create field_showroom'.
+  $permissions['create field_showroom'] = array(
+    'name' => 'create field_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'create field_tode_showroom'.
+  $permissions['create field_tode_showroom'] = array(
+    'name' => 'create field_tode_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'create showroom content'.
+  $permissions['create showroom content'] = array(
+    'name' => 'create showroom content',
+    'roles' => array(
+      'administrator' => 'administrator',
+    ),
+    'module' => 'node',
+  );
+
+  // Exported permission: 'delete any showroom content'.
+  $permissions['delete any showroom content'] = array(
+    'name' => 'delete any showroom content',
+    'roles' => array(
+      'administrator' => 'administrator',
+    ),
+    'module' => 'node',
+  );
+
+  // Exported permission: 'delete own showroom content'.
+  $permissions['delete own showroom content'] = array(
+    'name' => 'delete own showroom content',
+    'roles' => array(
+      'administrator' => 'administrator',
+    ),
+    'module' => 'node',
+  );
+
   // Exported permission: 'delete terms in showroom'.
   $permissions['delete terms in showroom'] = array(
     'name' => 'delete terms in showroom',
-    'roles' => array(),
+    'roles' => array(
+      'administrator' => 'administrator',
+    ),
     'module' => 'taxonomy',
   );
 
+  // Exported permission: 'delete users with role 15'.
+  $permissions['delete users with role 15'] = array(
+    'name' => 'delete users with role 15',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'administerusersbyrole',
+  );
+
+  // Exported permission: 'edit any showroom content'.
+  $permissions['edit any showroom content'] = array(
+    'name' => 'edit any showroom content',
+    'roles' => array(
+      'administrator' => 'administrator',
+    ),
+    'module' => 'node',
+  );
+
+  // Exported permission: 'edit field_showroom'.
+  $permissions['edit field_showroom'] = array(
+    'name' => 'edit field_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'edit field_tode_showroom'.
+  $permissions['edit field_tode_showroom'] = array(
+    'name' => 'edit field_tode_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'edit own field_showroom'.
+  $permissions['edit own field_showroom'] = array(
+    'name' => 'edit own field_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'edit own field_tode_showroom'.
+  $permissions['edit own field_tode_showroom'] = array(
+    'name' => 'edit own field_tode_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'edit own showroom content'.
+  $permissions['edit own showroom content'] = array(
+    'name' => 'edit own showroom content',
+    'roles' => array(
+      'administrator' => 'administrator',
+    ),
+    'module' => 'node',
+  );
+
   // Exported permission: 'edit terms in showroom'.
   $permissions['edit terms in showroom'] = array(
     'name' => 'edit terms in showroom',
-    'roles' => array(),
+    'roles' => array(
+      'administrator' => 'administrator',
+    ),
     'module' => 'taxonomy',
   );
 
+  // Exported permission: 'edit users with role 15'.
+  $permissions['edit users with role 15'] = array(
+    'name' => 'edit users with role 15',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'administerusersbyrole',
+  );
+
+  // Exported permission: 'enter showroom revision log entry'.
+  $permissions['enter showroom revision log entry'] = array(
+    'name' => 'enter showroom revision log entry',
+    'roles' => array(),
+    'module' => 'override_node_options',
+  );
+
   // Exported permission: 'merge showroom terms'.
   $permissions['merge showroom terms'] = array(
     'name' => 'merge showroom terms',
@@ -31,5 +177,95 @@ function showroom_user_default_permissions() {
     'module' => 'term_merge',
   );
 
+  // Exported permission: 'override showroom authored by option'.
+  $permissions['override showroom authored by option'] = array(
+    'name' => 'override showroom authored by option',
+    'roles' => array(),
+    'module' => 'override_node_options',
+  );
+
+  // Exported permission: 'override showroom authored on option'.
+  $permissions['override showroom authored on option'] = array(
+    'name' => 'override showroom authored on option',
+    'roles' => array(),
+    'module' => 'override_node_options',
+  );
+
+  // Exported permission: 'override showroom promote to front page option'.
+  $permissions['override showroom promote to front page option'] = array(
+    'name' => 'override showroom promote to front page option',
+    'roles' => array(),
+    'module' => 'override_node_options',
+  );
+
+  // Exported permission: 'override showroom published option'.
+  $permissions['override showroom published option'] = array(
+    'name' => 'override showroom published option',
+    'roles' => array(),
+    'module' => 'override_node_options',
+  );
+
+  // Exported permission: 'override showroom revision option'.
+  $permissions['override showroom revision option'] = array(
+    'name' => 'override showroom revision option',
+    'roles' => array(),
+    'module' => 'override_node_options',
+  );
+
+  // Exported permission: 'override showroom sticky option'.
+  $permissions['override showroom sticky option'] = array(
+    'name' => 'override showroom sticky option',
+    'roles' => array(),
+    'module' => 'override_node_options',
+  );
+
+  // Exported permission: 'show showroom title'.
+  $permissions['show showroom title'] = array(
+    'name' => 'show showroom title',
+    'roles' => array(),
+    'module' => 'materio_page_title',
+  );
+
+  // Exported permission: 'view field_showroom'.
+  $permissions['view field_showroom'] = array(
+    'name' => 'view field_showroom',
+    'roles' => array(
+      'Showroom' => 'Showroom',
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'view field_tode_showroom'.
+  $permissions['view field_tode_showroom'] = array(
+    'name' => 'view field_tode_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'view own field_showroom'.
+  $permissions['view own field_showroom'] = array(
+    'name' => 'view own field_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
+  // Exported permission: 'view own field_tode_showroom'.
+  $permissions['view own field_tode_showroom'] = array(
+    'name' => 'view own field_tode_showroom',
+    'roles' => array(
+      'administrator' => 'administrator',
+      'root' => 'root',
+    ),
+    'module' => 'field_permissions',
+  );
+
   return $permissions;
 }

+ 4 - 16
sites/all/modules/features/showroom/showroom.features.user_role.inc

@@ -10,22 +10,10 @@
 function showroom_user_default_roles() {
   $roles = array();
 
-  // Exported role: Translator CN.
-  $roles['Translator CN'] = array(
-    'name' => 'Translator CN',
-    'weight' => 12,
-  );
-
-  // Exported role: Translator EN.
-  $roles['Translator EN'] = array(
-    'name' => 'Translator EN',
-    'weight' => 10,
-  );
-
-  // Exported role: Translator FR.
-  $roles['Translator FR'] = array(
-    'name' => 'Translator FR',
-    'weight' => 11,
+  // Exported role: Showroom.
+  $roles['Showroom'] = array(
+    'name' => 'Showroom',
+    'weight' => 13,
   );
 
   return $roles;

+ 84 - 3
sites/all/modules/features/showroom/showroom.info

@@ -1,24 +1,105 @@
 name = Showroom
 core = 7.x
 package = Materio
+dependencies[] = addressfield
+dependencies[] = administerusersbyrole
+dependencies[] = cck_phone
 dependencies[] = ctools
+dependencies[] = email
 dependencies[] = features
+dependencies[] = field_permissions
 dependencies[] = materio_content_types
+dependencies[] = materio_page_title
+dependencies[] = materio_subscriptions
 dependencies[] = metatag
+dependencies[] = node
+dependencies[] = options
+dependencies[] = override_node_options
+dependencies[] = role_delegation
+dependencies[] = rules
 dependencies[] = strongarm
 dependencies[] = taxonomy
+dependencies[] = taxonomy_access
 dependencies[] = term_merge
 dependencies[] = text
+dependencies[] = tode
 features[ctools][] = strongarm:strongarm:1
 features[features_api][] = api:2
 features[field_base][] = description_field
+features[field_base][] = field_showroom
+features[field_base][] = field_tode_showroom
+features[field_instance][] = node-showroom-body
+features[field_instance][] = node-showroom-field_public_address
+features[field_instance][] = node-showroom-field_public_email
+features[field_instance][] = node-showroom-field_public_phone
+features[field_instance][] = node-showroom-field_tode_showroom
 features[field_instance][] = taxonomy_term-showroom-description_field
 features[field_instance][] = taxonomy_term-showroom-name_field
+features[field_instance][] = user-user-field_showroom
+features[node][] = showroom
+features[rules_config][] = rules_auto_tag_news_with_showroom
 features[taxonomy][] = showroom
+features[user_permission][] = assign Showroom role
+features[user_permission][] = create field_showroom
+features[user_permission][] = create field_tode_showroom
+features[user_permission][] = create showroom content
+features[user_permission][] = delete any showroom content
+features[user_permission][] = delete own showroom content
 features[user_permission][] = delete terms in showroom
+features[user_permission][] = delete users with role 15
+features[user_permission][] = edit any showroom content
+features[user_permission][] = edit field_showroom
+features[user_permission][] = edit field_tode_showroom
+features[user_permission][] = edit own field_showroom
+features[user_permission][] = edit own field_tode_showroom
+features[user_permission][] = edit own showroom content
 features[user_permission][] = edit terms in showroom
+features[user_permission][] = edit users with role 15
+features[user_permission][] = enter showroom revision log entry
 features[user_permission][] = merge showroom terms
-features[user_role][] = Translator CN
-features[user_role][] = Translator EN
-features[user_role][] = Translator FR
+features[user_permission][] = override showroom authored by option
+features[user_permission][] = override showroom authored on option
+features[user_permission][] = override showroom promote to front page option
+features[user_permission][] = override showroom published option
+features[user_permission][] = override showroom revision option
+features[user_permission][] = override showroom sticky option
+features[user_permission][] = show showroom title
+features[user_permission][] = view field_showroom
+features[user_permission][] = view field_tode_showroom
+features[user_permission][] = view own field_showroom
+features[user_permission][] = view own field_tode_showroom
+features[user_role][] = Showroom
+features[variable][] = additional_settings__active_tab_showroom
+features[variable][] = ant_pattern_showroom
+features[variable][] = ant_php_showroom
+features[variable][] = ant_showroom
+features[variable][] = date_popup_authored_enabled_showroom
+features[variable][] = date_popup_authored_format_showroom
+features[variable][] = date_popup_authored_year_range_showroom
+features[variable][] = diff_enable_revisions_page_node_showroom
+features[variable][] = diff_show_preview_changes_node_showroom
+features[variable][] = diff_view_mode_preview_node_showroom
+features[variable][] = entity_translation_hide_translation_links_showroom
+features[variable][] = entity_translation_node_metadata_showroom
+features[variable][] = field_bundle_settings_node__showroom
+features[variable][] = i18n_node_extended_showroom
+features[variable][] = i18n_node_options_showroom
+features[variable][] = language_content_type_showroom
+features[variable][] = menu_options_showroom
+features[variable][] = menu_parent_showroom
+features[variable][] = metatag_enable_node__showroom
+features[variable][] = metatag_enable_taxonomy_term__showroom
+features[variable][] = node_options_showroom
+features[variable][] = node_preview_showroom
+features[variable][] = node_submitted_showroom
+features[variable][] = nodeformscols_field_placements_showroom_default
+features[variable][] = print_html_display_comment_showroom
+features[variable][] = print_html_display_showroom
+features[variable][] = print_html_display_urllist_showroom
+features[variable][] = save_continue_showroom
+features[variable][] = simplenews_content_type_showroom
+features[variable][] = unique_field_comp_showroom
+features[variable][] = unique_field_fields_showroom
+features[variable][] = unique_field_scope_showroom
+features[variable][] = unique_field_show_matches_showroom
 project path = sites/all/modules/features

+ 31 - 0
sites/all/modules/features/showroom/showroom.rules_defaults.inc

@@ -0,0 +1,31 @@
+<?php
+/**
+ * @file
+ * showroom.rules_defaults.inc
+ */
+
+/**
+ * Implements hook_default_rules_configuration().
+ */
+function showroom_default_rules_configuration() {
+  $items = array();
+  $items['rules_auto_tag_news_with_showroom'] = entity_import('rules_config', '{ "rules_auto_tag_news_with_showroom" : {
+      "LABEL" : "auto tag news with showroom",
+      "PLUGIN" : "reaction rule",
+      "REQUIRES" : [ "rules" ],
+      "ON" : [ "node_presave" ],
+      "IF" : [
+        { "node_is_of_type" : { "node" : [ "node" ], "type" : { "value" : { "breve" : "breve" } } } },
+        { "user_has_role" : { "account" : [ "node:author" ], "roles" : { "value" : { "15" : "15" } } } }
+      ],
+      "DO" : [
+        { "data_set" : {
+            "data" : [ "node:field-showroom" ],
+            "value" : [ "node:author:field-showroom" ]
+          }
+        }
+      ]
+    }
+  }');
+  return $items;
+}

+ 336 - 0
sites/all/modules/features/showroom/showroom.strongarm.inc

@@ -0,0 +1,336 @@
+<?php
+/**
+ * @file
+ * showroom.strongarm.inc
+ */
+
+/**
+ * Implements hook_strongarm().
+ */
+function showroom_strongarm() {
+  $export = array();
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'additional_settings__active_tab_showroom';
+  $strongarm->value = 'edit-auto-nodetitle';
+  $export['additional_settings__active_tab_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'ant_pattern_showroom';
+  $strongarm->value = '<?php 
+$items = field_get_items(\'node\', $node, \'field_tode_showroom\');
+return t($items[0][\'name\']);
+?>
+';
+  $export['ant_pattern_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'ant_php_showroom';
+  $strongarm->value = 0;
+  $export['ant_php_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'ant_showroom';
+  $strongarm->value = '1';
+  $export['ant_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'date_popup_authored_enabled_showroom';
+  $strongarm->value = 1;
+  $export['date_popup_authored_enabled_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'date_popup_authored_format_showroom';
+  $strongarm->value = 'Y-m-d H:i';
+  $export['date_popup_authored_format_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'date_popup_authored_year_range_showroom';
+  $strongarm->value = '3';
+  $export['date_popup_authored_year_range_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'diff_enable_revisions_page_node_showroom';
+  $strongarm->value = 1;
+  $export['diff_enable_revisions_page_node_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'diff_show_preview_changes_node_showroom';
+  $strongarm->value = 1;
+  $export['diff_show_preview_changes_node_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'diff_view_mode_preview_node_showroom';
+  $strongarm->value = 'full';
+  $export['diff_view_mode_preview_node_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'entity_translation_hide_translation_links_showroom';
+  $strongarm->value = 0;
+  $export['entity_translation_hide_translation_links_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'entity_translation_node_metadata_showroom';
+  $strongarm->value = '0';
+  $export['entity_translation_node_metadata_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'field_bundle_settings_node__showroom';
+  $strongarm->value = array(
+    'view_modes' => array(),
+    'extra_fields' => array(
+      'form' => array(
+        'metatags' => array(
+          'weight' => '6',
+        ),
+        'title' => array(
+          'weight' => '0',
+        ),
+        'path' => array(
+          'weight' => '3',
+        ),
+        'redirect' => array(
+          'weight' => '4',
+        ),
+        'language' => array(
+          'weight' => '2',
+        ),
+      ),
+      'display' => array(),
+    ),
+  );
+  $export['field_bundle_settings_node__showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'i18n_node_extended_showroom';
+  $strongarm->value = 1;
+  $export['i18n_node_extended_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'i18n_node_options_showroom';
+  $strongarm->value = array();
+  $export['i18n_node_options_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'language_content_type_showroom';
+  $strongarm->value = '4';
+  $export['language_content_type_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'menu_options_showroom';
+  $strongarm->value = array();
+  $export['menu_options_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'menu_parent_showroom';
+  $strongarm->value = 'main-menu:0';
+  $export['menu_parent_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'metatag_enable_node__showroom';
+  $strongarm->value = TRUE;
+  $export['metatag_enable_node__showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'metatag_enable_taxonomy_term__showroom';
+  $strongarm->value = TRUE;
+  $export['metatag_enable_taxonomy_term__showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'nodeformscols_field_placements_showroom_default';
+  $strongarm->value = array(
+    'additional_settings' => array(
+      'region' => 'main',
+      'weight' => '5',
+      'has_required' => FALSE,
+      'title' => 'Onglets verticaux',
+      'hidden' => 0,
+    ),
+    'actions' => array(
+      'region' => 'right',
+      'weight' => '2',
+      'has_required' => FALSE,
+      'title' => 'Enregistrer',
+      'hidden' => 0,
+    ),
+    'entity_translation_entity_form_language_update' => array(
+      'region' => 'right',
+      'weight' => '1',
+      'has_required' => FALSE,
+      'title' => NULL,
+      'hidden' => 0,
+    ),
+    'language' => array(
+      'region' => 'right',
+      'weight' => '0',
+      'has_required' => FALSE,
+      'title' => 'Langue',
+      'hidden' => 0,
+    ),
+    'body' => array(
+      'region' => 'main',
+      'weight' => '1',
+      'has_required' => FALSE,
+      'title' => 'Body',
+      'hidden' => 0,
+    ),
+    'field_tode_showroom' => array(
+      'region' => 'main',
+      'weight' => '0',
+      'has_required' => FALSE,
+      'title' => 'Showroom',
+      'hidden' => 0,
+    ),
+    'field_public_address' => array(
+      'region' => 'main',
+      'weight' => '4',
+      'has_required' => TRUE,
+      'title' => 'Adresse',
+    ),
+    'field_public_email' => array(
+      'region' => 'main',
+      'weight' => '2',
+      'has_required' => FALSE,
+      'title' => 'Email',
+      'hidden' => 0,
+    ),
+    'field_public_phone' => array(
+      'region' => 'main',
+      'weight' => '3',
+      'has_required' => FALSE,
+      'title' => 'Phone',
+      'hidden' => 0,
+    ),
+  );
+  $export['nodeformscols_field_placements_showroom_default'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'node_options_showroom';
+  $strongarm->value = array(
+    0 => 'status',
+  );
+  $export['node_options_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'node_preview_showroom';
+  $strongarm->value = '0';
+  $export['node_preview_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'node_submitted_showroom';
+  $strongarm->value = 0;
+  $export['node_submitted_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'print_html_display_comment_showroom';
+  $strongarm->value = 0;
+  $export['print_html_display_comment_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'print_html_display_showroom';
+  $strongarm->value = 0;
+  $export['print_html_display_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'print_html_display_urllist_showroom';
+  $strongarm->value = 0;
+  $export['print_html_display_urllist_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'save_continue_showroom';
+  $strongarm->value = 'Enregistrer et ajouter les champs';
+  $export['save_continue_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'simplenews_content_type_showroom';
+  $strongarm->value = 0;
+  $export['simplenews_content_type_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'unique_field_comp_showroom';
+  $strongarm->value = 'each';
+  $export['unique_field_comp_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'unique_field_fields_showroom';
+  $strongarm->value = array();
+  $export['unique_field_fields_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'unique_field_scope_showroom';
+  $strongarm->value = 'type';
+  $export['unique_field_scope_showroom'] = $strongarm;
+
+  $strongarm = new stdClass();
+  $strongarm->disabled = FALSE; /* Edit this to true to make a default strongarm disabled initially */
+  $strongarm->api_version = 1;
+  $strongarm->name = 'unique_field_show_matches_showroom';
+  $strongarm->value = array();
+  $export['unique_field_show_matches_showroom'] = $strongarm;
+
+  return $export;
+}