diff --git a/sites/all/modules/contrib/dev/node_access_rebuild_progressive/LICENSE.txt b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/LICENSE.txt new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/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. + + + Copyright (C) + + 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. + + , 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. diff --git a/sites/all/modules/contrib/dev/node_access_rebuild_progressive/README.md b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/README.md new file mode 100644 index 00000000..e69de29b diff --git a/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.drush.inc b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.drush.inc new file mode 100644 index 00000000..44ebccd4 --- /dev/null +++ b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.drush.inc @@ -0,0 +1,56 @@ + "Fully rebuild node access.", + 'callback' => 'drush_node_access_rebuild_progressive_rebuild', + ); + + return $items; +} + +/** + * Rebuilds the node access grants table. + */ +function drush_node_access_rebuild_progressive_rebuild() { + node_access_rebuild_progressive_trigger(); + $batch_def = array( + 'title' => t('Node access rebuild'), + 'operations' => array( + array('node_access_rebuild_progressive_process_drush', array()), + ), + 'file' => 'node_access_rebuild_progressive.drush.inc', + 'finished' => 'node_access_rebuild_progressive_finished', + ); + + batch_set($batch_def); + $batch = & batch_get(); + $batch['progressive'] = FALSE; + drush_backend_batch_process(); +} + +function node_access_rebuild_progressive_process_drush(&$context) { + if (!isset($context['sandbox']['total'])) { + $context['sandbox']['total'] = 0; + $context['sandbox']['processed'] = 0; + $context['sandbox']['nodes'] = db_query("SELECT count(nid) FROM {node}")->fetchField(); + } + $pass = node_access_rebuild_progressive_process_chunk(); + $context['sandbox']['total'] += $pass['total']; + $context['sandbox']['processed'] += $pass['processed']; + $numbers = array( + '@processed' => $pass['total'], + '@total' => $pass['processed'], + '@grand_processed' => $context['sandbox']['processed'], + '@grand_total' => $context['sandbox']['total'], + '@nodes' => $context['sandbox']['nodes'], + ); + $context['finished'] = empty($pass['total']) ? 1 : 0; + if (!$context['finished']) { + drush_print(dt('Processed @processed of @total nodes - Total: @grand_processed/@grand_total (@nodes)', $numbers)); + } +} diff --git a/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.info b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.info new file mode 100644 index 00000000..482a2744 --- /dev/null +++ b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.info @@ -0,0 +1,10 @@ +name = Node Access Rebuild Progressive +description = "Rebuild node access grants in chunks" +core = 7.x + +; Information added by Drupal.org packaging script on 2017-10-11 +version = "7.x-1.0" +core = "7.x" +project = "node_access_rebuild_progressive" +datestamp = "1507730050" + diff --git a/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.module b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.module new file mode 100644 index 00000000..c5374317 --- /dev/null +++ b/sites/all/modules/contrib/dev/node_access_rebuild_progressive/node_access_rebuild_progressive.module @@ -0,0 +1,158 @@ + 'Node access rebuild', + 'description' => t('Node access progressive rebuild settings.'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('node_access_rebuild_progressive_admin_form'), + 'access arguments' => array('administer site configuration'), + ); + + return $items; +} + +/** + * Module settings form callback. + */ +function node_access_rebuild_progressive_admin_form() { + $form = array(); + + $form['node_access_rebuild_progressive_cron'] = array( + '#type' => 'checkbox', + '#title' => t('Enable progressive node access rebuild at cron time'), + '#description' => t('It is recommended that you run cron via drush for using this feature.'), + '#default_value' => variable_get('node_access_rebuild_progressive_cron', FALSE), + ); + + $form['node_access_rebuild_progressive_chunk'] = array( + '#type' => 'textfield', + '#title' => 'Number of nodes to process in each chunk', + '#default_value' => variable_get('node_access_rebuild_progressive_chunk', 500), + '#description' => t('The number of nodes that will be processed per cron run. Make sure it can safely fit in memory, and in the cron run time if you are not running cron via drush.'), + ); + return system_settings_form($form); +} + +/** + * Validation callback. + */ +function node_access_rebuild_progressive_admin_form_validate($form, &$form_state) { + $chunk_size = $form_state['values']['node_access_rebuild_progressive_chunk']; + if (!is_numeric($chunk_size) || $chunk_size <= 0) { + form_set_error('node_access_rebuild_progressive_chunk', t('Chunk size must be a positive integer.')); + } +} + +/** + * Implements hook_cron(). + */ +function node_access_rebuild_progressive_cron() { + // Not enabled on cron, nothing to do. + if (!variable_get('node_access_rebuild_progressive_cron', FALSE)) { + return; + } + // Trigger the processing. + if (node_access_needs_rebuild()) { + node_access_rebuild_progressive_trigger(); + } + // Process a batch of nodes if needed. + if (variable_get('node_access_rebuild_progressive_current', 0) > 0) { + node_access_rebuild_progressive_process_cron(); + } +} + +/** + * Initiate the full rebuild. + */ +function node_access_rebuild_progressive_trigger() { + // Prevents default message/behaviour. + node_access_needs_rebuild(FALSE); + // Add default grants in the unlikely case + // no modules implement node_grants anymore. + if (!count(module_implements('node_grants'))) { + node_access_rebuild_progressive_set_default(); + return node_access_rebuild_progressive_finished(); + } + // Clean up possible mismatches. + db_query("DELETE FROM {node_access} WHERE nid NOT IN (SELECT nid FROM {node})"); + // We only need the current one, nodes created afterward would get processed + // at save time. + $highest = db_query("SELECT nid FROM {node} ORDER BY nid DESC LIMIT 0,1")->fetchField(); + $count = db_query("SELECT count(nid) FROM {node}")->fetchField(); + variable_set('node_access_rebuild_progressive_current', $highest + 1); + watchdog('node_access_rebuild_progressive', '%count nodes queued for node access rebuild.', array('%count' => $count), WATCHDOG_NOTICE); +} + +function node_access_rebuild_progressive_set_default() { + db_truncate('node_access')->execute(); + db_insert('node_access') + ->fields(array( + 'nid' => 0, + 'realm' => 'all', + 'gid' => 0, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + )) + ->execute(); +} + +/** + * Processes a chunk of nodes at cron run. + */ +function node_access_rebuild_progressive_process_cron() { + $pass = node_access_rebuild_progressive_process_chunk(); + if (empty($pass['total'])) { + return node_access_rebuild_progressive_finished(); + } +} + +/** + * Processes a chunk of nodes. + * @return array + * An array with the following keys: + * - total: the number of nodes retrieved for processing. + * - processed: the number of nodes actually processed. + */ +function node_access_rebuild_progressive_process_chunk() { + $current = variable_get('node_access_rebuild_progressive_current', 0); + $chunk_size = (int) variable_get('node_access_rebuild_progressive_chunk', 500); + $nids = db_query("SELECT nid FROM {node} WHERE nid < :current ORDER BY nid DESC LIMIT 0,$chunk_size", array(':current' => $current))->fetchCol(); + $total = count($nids); + $processed = 0; + if ($total) { + $nodes = node_load_multiple($nids); + foreach ($nodes as $node) { + // Make sure the node can be loaded properly. + if (!empty($node->nid)) { + node_access_acquire_grants($node); + $current = $node->nid; + $processed ++; + } + } + variable_set('node_access_rebuild_progressive_current', $current); + } + return array( + 'total' => $total, + 'processed' => $processed, + ); +} + +/** + * Cleanup after queue completion. + */ +function node_access_rebuild_progressive_finished() { + variable_set('node_access_rebuild_progressive_current', 0); + watchdog('node_access_rebuild_progressive', 'Node access rebuild finished.', NULL, WATCHDOG_NOTICE); +} diff --git a/sites/all/modules/contrib/security/login_notify/LICENSE.txt b/sites/all/modules/contrib/security/login_notify/LICENSE.txt new file mode 100644 index 00000000..d159169d --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/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. + + + Copyright (C) + + 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. + + , 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. diff --git a/sites/all/modules/contrib/security/login_notify/README.txt b/sites/all/modules/contrib/security/login_notify/README.txt new file mode 100644 index 00000000..dd14680a --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/README.txt @@ -0,0 +1,63 @@ + +INTRODUCTION +------------ + +The purpose of Login Notify is to alert you when someone logs in to your +account from an unrecognized browser. It also gives you the opportunity to +"lock out" a browser if you wish. + +The goal of this is to minimize the damage that can be done by a malicious user +in the event that someone obtains your password, or through some other means +manages to log in to your account. + +This module was inspired by a similar Facebook feature introduced in 2010. + http://www.facebook.com/blog/blog.php?post=389991097130 + + +INSTALLATION +------------ + +Install this module using the typical process: + +1. Copy the module to the sites/SITENAME/modules directory. + +2. Enable the module in admin/modules or via drush. + +3. Configure the settings in admin/config/people/login-notify to enable login + notification and select the roles that require it. The default settings are + "All roles except those selected below" with all roles unchecked. This will + require login notification for all users with roles other than "anonymous + user" and "authenticated user". + + +HOW IT WORKS +------------ + +When someone logs in to the site for the first time from an unrecognized +browser they are required to give the browser a unique name before they can +proceed. An email is then sent to the email address on record for the account. +This email contains a link that allows you to "lock out" the browser, which +will forcefully log them out the next time they access the site from your +account. + +Browsers are recognized by storing an additional cookie that persists even +after logging out. If that cookie doesn't exist, or if it doesn't match a valid +record in the {login_notify_browsers} table, the browser is unrecognized. + +Browser records in the {login_notify_browsers} table are unique for each user +account, so if someone uses multiple accounts on the same site they'll have to +name their browser once for every account. + +Despite the name of the module (Login Notify), it does not implement +hook_user_login(). Instead it implements hook_init(). There are two reasons for +this: + +1. In case someone accesses site without going through the login process. For + example, by hijacking the session cookie. + +2. In the event that a browser is "locked out" it needs to be logged out + immediately on the next page load without waiting until the next time they + log in. + +From a developer standpoint the module can better be thought of as "Browser +Authentication" rather than "Login Notification". diff --git a/sites/all/modules/contrib/security/login_notify/login_notify.admin.inc b/sites/all/modules/contrib/security/login_notify/login_notify.admin.inc new file mode 100644 index 00000000..657a35fa --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/login_notify.admin.inc @@ -0,0 +1,137 @@ + 'checkbox', + '#title' => t('Enable login notification'), + '#default_value' => variable_get('login_notify_enabled', 0), + '#description' => t('This will require users with the below roles to register their browser by giving it a unique name. When a new browser is registered a notification email will be sent to the user.'), + ); + + // Exclude anonymous. + $roles = array_values(user_roles(TRUE)); + sort($roles); + $roles = drupal_map_assoc($roles); + // Also exclude 'authenticated user'. + unset($roles['authenticated user']); + + // The default setting for roles mode is 'exclude' and for roles is an empty + // array, which will include all users with any role other than 'anonymous' + // and 'authenticated user'. + $form['roles'] = array( + '#type' => 'fieldset', + '#title' => t('Roles'), + 'login_notify_roles_mode' => array( + '#type' => 'radios', + '#title' => t('Roles which require login notification'), + '#options' => array( + 'all' => t('All authenticated users'), + 'include' => t('Only the roles selected below'), + 'exclude' => t('All roles except those selected below'), + ), + '#default_value' => variable_get('login_notify_roles_mode', 'exclude'), + ), + 'login_notify_roles' => array( + '#type' => 'checkboxes', + '#title' => t('Roles'), + '#options' => drupal_map_assoc($roles), + '#default_value' => variable_get('login_notify_roles', array('')), + '#states' => array( + 'invisible' => array( + ':input[name="login_notify_roles_exclude"]' => array('value' => 'all'), + ), + ), + ), + ); + + $form['email_title'] = array( + '#type' => 'item', + '#title' => t('E-mails'), + ); + $form['email'] = array( + '#type' => 'vertical_tabs', + ); + + $form['email']['new'] = array( + '#type' => 'fieldset', + '#title' => t('New browser'), + '#description' => t('Edit the notification sent when an unrecognized browser is registered.'), + 'login_notify_email_subject' => array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#required' => TRUE, + '#default_value' => _login_notify_mail_text('login_notify_email_subject', NULL, FALSE), + ), + 'login_notify_email_body' => array( + '#type' => 'textarea', + '#title' => t('Body'), + '#required' => TRUE, + '#default_value' => _login_notify_mail_text('login_notify_email_body', NULL, FALSE), + ), + ); + + $form['email']['bypass_user_agent'] = array( + '#type' => 'fieldset', + '#title' => t('Unregistered browser'), + '#description' => t('Edit the notification sent when an unregistered browser is allowed to access the site without being named.'), + 'login_notify_email_bypass_user_agent_subject' => array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#required' => TRUE, + '#default_value' => _login_notify_mail_text('login_notify_email_bypass_user_agent_subject', NULL, FALSE), + ), + 'login_notify_email_bypass_user_agent_body' => array( + '#type' => 'textarea', + '#title' => t('Body'), + '#required' => TRUE, + '#default_value' => _login_notify_mail_text('login_notify_email_bypass_user_agent_body', NULL, FALSE), + ), + ); + + $form['email']['blocked'] = array( + '#type' => 'fieldset', + '#title' => t('Browser blocked'), + '#description' => t('Edit the notification sent when a locked browser attempts to access the site.'), + 'login_notify_email_blocked_subject' => array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#required' => TRUE, + '#default_value' => _login_notify_mail_text('login_notify_email_blocked_subject', NULL, FALSE), + ), + 'login_notify_email_blocked_body' => array( + '#type' => 'textarea', + '#title' => t('Body'), + '#required' => TRUE, + '#default_value' => _login_notify_mail_text('login_notify_email_blocked_body', NULL, FALSE), + ), + ); + + // @todo: Some way to display the available tokens if the token module isn't + // installed. + if (module_exists('token')) { + // Add the token tree UI. + $form['email']['token_tree'] = array( + '#theme' => 'token_tree', + '#token_types' => array('user', 'browser'), + '#show_restricted' => TRUE, + '#dialog' => TRUE, + '#weight' => 90, + ); + } + + return system_settings_form($form); +} diff --git a/sites/all/modules/contrib/security/login_notify/login_notify.info b/sites/all/modules/contrib/security/login_notify/login_notify.info new file mode 100644 index 00000000..c6afaf5f --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/login_notify.info @@ -0,0 +1,11 @@ +name = Login Notify +description = Notifies users when someone logs in to their account from an unrecognized browser. +core = 7.x +configure = admin/config/people/login-notify + +; Information added by drupal.org packaging script on 2013-10-18 +version = "7.x-1.0" +core = "7.x" +project = "login_notify" +datestamp = "1382110102" + diff --git a/sites/all/modules/contrib/security/login_notify/login_notify.install b/sites/all/modules/contrib/security/login_notify/login_notify.install new file mode 100644 index 00000000..41cef60c --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/login_notify.install @@ -0,0 +1,156 @@ + 'Browsers identified by users.', + 'fields' => array( + 'browser_id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'The unique ID of this browser.', + ), + 'uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'description' => 'User ID', + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'description' => 'The unique name chosen by the user that identifies the browser.', + ), + 'token' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'description' => 'The random token that identifies the browser in the cookie.', + ), + 'locked' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Boolean indicating if this browser has been locked.', + ), + 'deleted' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Boolean indicating if this browser has been deleted.', + ), + 'bypass_user_agent' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Boolean indicating if this user agent can bypass the browser naming process.', + ), + 'remember' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 1, + 'description' => 'Boolean indicating if the user chose to remember the browser.', + ), + 'ip_address' => array( + 'type' => 'varchar', + 'length' => '15', + 'not null' => TRUE, + 'default' => '', + 'description' => 'The IP address of the user when this browser was added (IPv4 only).', + ), + 'user_agent' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The user agent of the browser when it was added.', + ), + 'created' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Timestamp for when the user added this browser.', + ), + 'access' => array( + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Timestamp for previous time user accessed the site.', + ), + ), + 'primary key' => array('browser_id'), + 'unique keys' => array( + 'uid_token' => array('uid', 'token'), + // Ideally we'd have a unique key for array('uid', 'name') but we can't + // because when a browser is 'deleted' it isn't actually deleted, it's + // just updated with 'deleted' => 1, and we need to allow someone to use + // the same name as a browser that has been deleted. + ), + 'foreign keys' => array( + 'uid' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + ), + 'indexes' => array( + 'uid' => array('uid'), + 'name' => array('name'), + 'token' => array('token'), + 'access' => array('access'), + 'uid_bypass_user_agent_user_agent' => array( + 'uid', + 'bypass_user_agent', + 'user_agent', + ), + ), + ); + return $schema; +} + +/** + * Adds the field bypass_user_agent to the table login_notify_browsers. + */ +function login_notify_update_7100() { + db_add_field('login_notify_browsers', 'bypass_user_agent', array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Boolean indicating if this user agent can bypass the browser naming process.', + )); + db_add_index('login_notify_browsers', 'uid_bypass_user_agent_user_agent', array( + 'uid', + 'bypass_user_agent', + 'user_agent', + )); +} diff --git a/sites/all/modules/contrib/security/login_notify/login_notify.module b/sites/all/modules/contrib/security/login_notify/login_notify.module new file mode 100644 index 00000000..418dd1a8 --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/login_notify.module @@ -0,0 +1,769 @@ +uid) && $_GET['q'] == 'user/' . $user->uid . '/browsers/name') { + return; + } + // Don't act on a valid, secure unlock page. + // This must work even if the current user is logged out because they'll be + // blocked immediately when they log in, so the only way to unlock the + // browser is to do it while logged out. + elseif (preg_match('/^user\/(\d+)\/browsers\/browser\/(\d+)\/unlock$/', $_GET['q'], $matches) + && !empty($_GET['timestamp']) + && !empty($_GET['key']) + && ($account = user_load($matches[1])) + && ($browser = login_notify_browser_load($matches[2])) + // Make sure the browser matches the account. + && $browser['uid'] == $account->uid) { + // Call the secure unlock callback. + module_load_include('pages.inc', 'login_notify'); + login_notify_user_browsers_unlock_secure($account, $browser, $_GET['timestamp'], $_GET['key']); + } + + // Check if this user requires login notification. + if (login_notify_user_requires_login_notify($user)) { + // Get the browser. + $browser = login_notify_browser_load_current($user); + $bypass_user_agent_browser = !$browser ? login_notify_browser_load_current_bypass_user_agent($user) : NULL; + if (!empty($browser['locked'])) { + // This device is locked. + // Send the notification email. + login_notify_browser_blocked_email_send($user, $browser); + + // Don't delete the cookie because it's shared by all user accounts and + // we only want to lock them out of this account. Otherwise it would + // force them to rename the browser in any other account they use. Plus, + // this sort of prevents them from just logging in again (though that + // can easily be bypassed if they just delete the cookie themselves). + // @todo: Need a way to unlock the browser in case someone accidently + // locks their own browser and doesn't know to delete the cookie. + // Log them out. + // Destroy the current session, and reset $user to the anonymous user. + session_destroy(); + + print '

' . t('This browser has been locked out of this account.') . '

'; + print '

' . t('You have been logged out.') . '

'; + drupal_exit(); + } + elseif ((!$browser || !empty($browser['deleted'])) + && !$bypass_user_agent_browser) { + // They either didn't have a cookie or it wasn't valid. + // Redirect them to the page to name their browser. + // Need to use login_notify_get_destination() so that it goes to our URL + // and not the 'destination' query string param, if there is one. + drupal_goto('user/' . $user->uid . '/browsers/name', array('query' => login_notify_get_destination())); + } + else { + // Valid browser cookie or this user agent can bypass the naming process. + // Update the last access time. + $browser = $browser ? $browser : $bypass_user_agent_browser; + $browser['access'] = REQUEST_TIME; + login_notify_browser_save($browser); + if (!empty($browser['remember'])) { + // This is a rolling cookie, we set it on every page load to push back + // the expire date. + login_notify_cookie_set($user, $browser); + } + if ($bypass_user_agent_browser) { + // This user agent can bypass the naming process but we still send an + // alert email. Change the IP address so that it's identified correctly + // in the email. + $bypass_user_agent_browser['ip_address'] = ip_address(); + login_notify_browser_bypass_user_agent_email_send($user, $bypass_user_agent_browser); + } + } + } +} + +/** + * Implements hook_menu(). + */ +function login_notify_menu() { + // @todo: Should this be merged into admin/config/people/accounts? + $items['admin/config/people/login-notify'] = array( + 'title' => 'Login notification settings', + 'description' => 'Configure which users require browser registration and the notification email text.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('login_notify_admin_form'), + 'access arguments' => array('administer site configuration'), + 'file' => 'login_notify.admin.inc', + ); + + $items['user/%user/browsers'] = array( + 'title' => 'Registered Browsers', + 'page callback' => 'login_notify_user_browsers', + 'page arguments' => array(1), + 'access callback' => 'login_notify_user_browsers_access', + 'access arguments' => array(1), + 'file' => 'login_notify.pages.inc', + 'type' => MENU_LOCAL_TASK, + ); + + $items['user/%user/browsers/name'] = array( + 'title' => 'Name your browser', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('login_notify_user_browsers_name_form', 1), + 'access callback' => 'login_notify_user_browsers_name_access', + 'access arguments' => array(1), + 'file' => 'login_notify.pages.inc', + ); + + $items['user/%user/browsers/browser/%login_notify_browser'] = array( + 'title' => 'Browser', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('login_notify_user_browsers_edit_form', 1, 4), + 'access callback' => 'login_notify_user_browsers_browser_access', + 'access arguments' => array(1, 4), + 'file' => 'login_notify.pages.inc', + ); + $items['user/%user/browsers/browser/%login_notify_browser/edit'] = array( + 'title' => 'Edit Browser', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['user/%user/browsers/browser/%login_notify_browser/delete'] = array( + 'title' => 'Delete Browser', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('login_notify_user_browsers_delete_form', 1, 4), + 'access callback' => 'login_notify_user_browsers_browser_access', + 'access arguments' => array(1, 4), + 'file' => 'login_notify.pages.inc', + ); + $items['user/%user/browsers/browser/%login_notify_browser/lock'] = array( + 'title' => 'Lock Browser', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('login_notify_user_browsers_lock_form', 1, 4), + 'access callback' => 'login_notify_user_browsers_browser_access', + 'access arguments' => array(1, 4), + 'file' => 'login_notify.pages.inc', + ); + $items['user/%user/browsers/browser/%login_notify_browser/unlock'] = array( + 'title' => 'Unlock Browser', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('login_notify_user_browsers_unlock_form', 1, 4), + 'access callback' => 'login_notify_user_browsers_browser_access', + 'access arguments' => array(1, 4), + 'file' => 'login_notify.pages.inc', + ); + + return $items; +} + +/** + * Menu access callback for user/%user/browsers. + */ +function login_notify_user_browsers_access($account) { + // Is login notification enabled? + if (!variable_get('login_notify_enabled', 0)) { + return FALSE; + } + + // Only allow someone to view the registered browsers of a user they're + // allowed to edit. + if (!user_edit_access($account)) { + return FALSE; + } + + // Make sure this user requires login notification. + if (!login_notify_user_requires_login_notify($account)) { + return FALSE; + } + + return TRUE; +} + +/** + * Menu access callback for user/%user/browsers/name. + */ +function login_notify_user_browsers_name_access($account) { + global $user; + + // Make sure we can access user/%user/browsers. + if (!login_notify_user_browsers_access($account)) { + return FALSE; + } + + // Can only name their own browser. + return !empty($account->uid) && !empty($user->uid) && $account->uid == $user->uid; +} + +/** + * Menu access callback for user/%user/browsers/browser/%. + */ +function login_notify_user_browsers_browser_access($account, $browser) { + if (!user_edit_access($account)) { + return FALSE; + } + + // Make sure we can access user/%user/browsers. + if (!login_notify_user_browsers_access($account)) { + return FALSE; + } + + // Make sure it's this user's browser. + return $browser['uid'] == $account->uid; +} + +/** + * Implements hook_admin_paths(). + * + * Sets the browser pages to use the admin theme. + */ +function login_notify_admin_paths() { + return array( + 'user/*/browsers' => TRUE, + 'user/*/browsers/*' => TRUE, + ); +} + +/** + * Implements hook_cron(). + * + * Deletes browsers that haven't accessed the site recently. + */ +function login_notify_cron() { + // Delete all the browsers that are too old. + // @todo: Should we also lock them? + db_update('login_notify_browsers') + ->fields(array('deleted' => TRUE)) + ->condition('access', REQUEST_TIME - LOGIN_NOTIFY_COOKIE_LENGTH, '<') + ->execute(); +} + +/** + * Determines if the given user requires login notification. + * + * @param object $account + * The user account to check. + * + * @return bool + * TRUE if the given user requires login notification. + */ +function login_notify_user_requires_login_notify($account) { + if (empty($account->uid)) { + return FALSE; + } + + $login_notify_roles_mode = variable_get('login_notify_roles_mode', 'exclude'); + $login_notify_roles = variable_get('login_notify_roles', array('')); + + if ($login_notify_roles_mode == 'all') { + // All authenticated users require login notification. + return TRUE; + } + + $roles = drupal_map_assoc($account->roles); + unset($roles['authenticated user']); + + if ($login_notify_roles_mode == 'include') { + // See if the user has any of the included roles. + foreach ($roles as $role) { + if (!empty($login_notify_roles[$role])) { + // They have a role that requires login notification. + return TRUE; + } + } + // Don't have any roles that are included. + return FALSE; + } + elseif ($login_notify_roles_mode == 'exclude') { + // See if the user has any roles that aren't excluded. + foreach ($roles as $role) { + if (empty($login_notify_roles[$role])) { + // They have a role that isn't excluded. + return TRUE; + } + } + // All of their roles are excluded. + return FALSE; + } + else { + // Shouldn't ever get here. + return FALSE; + } +} + +/** + * Loads the current browser record from the database for the given user. + * + * @param object $account + * The user account whose browser record to load from the DB. + * + * @return array + * A browser record in the DB, or NULL if none is found. + */ +function login_notify_browser_load_current($account) { + if (!empty($_COOKIE[LOGIN_NOTIFY_COOKIE_NAME])) { + return login_notify_browser_load_by_token($account->uid, $_COOKIE[LOGIN_NOTIFY_COOKIE_NAME]); + } + else { + return NULL; + } +} + +/** + * Loads the current browser record that's allowed to bypass browser naming. + * + * @param object $account + * The user account whose browser record to load from the DB. + * + * @return array + * A browser record in the DB, or NULL if none is found. + */ +function login_notify_browser_load_current_bypass_user_agent($account) { + // Blank user agents are not allowed to bypass the browser naming process. + if (!empty($_SERVER['HTTP_USER_AGENT'])) { + return login_notify_browser_load_by_bypass_user_agent($account->uid, $_SERVER['HTTP_USER_AGENT']); + } + else { + return NULL; + } +} + +/** + * Sets the cookie for the given user account and browser record. + * + * @param object $account + * The user account. + * @param array $browser + * The browser record, as it is (or will be) in the table + * login_notify_browsers. + */ +function login_notify_cookie_set($account, $browser, $remember_me = TRUE) { + $expire = $remember_me ? REQUEST_TIME + LOGIN_NOTIFY_COOKIE_LENGTH : 0; + setcookie(LOGIN_NOTIFY_COOKIE_NAME, $browser['token'], $expire, '/'); +} + +/** + * Deletes the cookie for the given user account. + * + * @param object $account + * The user account whose browser cookie to delete. + */ +function login_notify_cookie_delete($account) { + setcookie(LOGIN_NOTIFY_COOKIE_NAME, '', REQUEST_TIME - 3600, '/'); +} + +/** + * Sends the browser notification email to the user. + * + * The email notifies them that a new browser has accessed the site using their + * account. + * + * @param object $account + * The user account to send the email to. + * @param array $browser + * The browser record as it is in the database. + * + * @see login_notify_mail() + */ +function login_notify_browser_email_send($account, $browser) { + $params = array( + 'account' => $account, + 'browser' => $browser, + ); + drupal_mail('login_notify', 'browser', $account->mail, language_default(), $params); +} + +/** + * Sends the unregistered browser notification email to the user. + * + * The email notifies them that an unregistered browser has accessed the site + * using their account. + * + * @param object $account + * The user account to send the email to. + * @param array $browser + * The browser record as it is in the database. + * + * @see login_notify_mail() + */ +function login_notify_browser_bypass_user_agent_email_send($account, $browser) { + $params = array( + 'account' => $account, + 'browser' => $browser, + ); + drupal_mail('login_notify', 'browser_bypass_user_agent', $account->mail, language_default(), $params); +} + +/** + * Sends the 'blocked' notification email to the user. + * + * The email notifies them that a locked browser attempted to access the site + * using their account. + * + * @param object $account + * The user account to send the email to. + * @param array $browser + * The browser record as it is in the database. + * + * @see login_notify_mail() + */ +function login_notify_browser_blocked_email_send($account, $browser) { + $params = array( + 'account' => $account, + 'browser' => $browser, + ); + drupal_mail('login_notify', 'browser_blocked', $account->mail, language_default(), $params); +} + +/** + * Implements hook_mail(). + * + * Sends an email to the user notifying them that a new browser has accessed + * the site using their account. + * + * @see login_notify_browser_email_send() + */ +function login_notify_mail($key, &$message, $params) { + switch ($key) { + case 'browser': + $account = $params['account']; + $browser = $params['browser']; + + $language = user_preferred_language($account); + + $token_data = array( + 'user' => $account, + 'browser' => $browser, + ); + + $message['subject'] = _login_notify_mail_text('login_notify_email_subject', $language, TRUE, $token_data); + $message['body'][] = _login_notify_mail_text('login_notify_email_body', $language, TRUE, $token_data); + break; + + case 'browser_bypass_user_agent': + $account = $params['account']; + $browser = $params['browser']; + + $language = user_preferred_language($account); + + $token_data = array( + 'user' => $account, + 'browser' => $browser, + ); + + $message['subject'] = _login_notify_mail_text('login_notify_email_bypass_user_agent_subject', $language, TRUE, $token_data); + $message['body'][] = _login_notify_mail_text('login_notify_email_bypass_user_agent_body', $language, TRUE, $token_data); + break; + + case 'browser_blocked': + $account = $params['account']; + $browser = $params['browser']; + + $language = user_preferred_language($account); + + $token_data = array( + 'user' => $account, + 'browser' => $browser, + ); + + $message['subject'] = _login_notify_mail_text('login_notify_email_blocked_subject', $language, TRUE, $token_data); + $message['body'][] = _login_notify_mail_text('login_notify_email_blocked_body', $language, TRUE, $token_data); + break; + } +} + +/** + * Returns a mail string for a variable name. + * + * This is similar to _user_mail_text(). + * + * @see login_notify_mail() + * @see _user_mail_text() + */ +function _login_notify_mail_text($key, $language = NULL, $replace = TRUE, $token_data = array()) { + $langcode = isset($language) ? $language->language : NULL; + + if ($admin_setting = variable_get($key, FALSE)) { + // An admin setting overrides the default string. + $text = $admin_setting; + } + else { + // No override, return default string. + switch ($key) { + case 'login_notify_email_subject': + $text = t('[site:name] login from "[browser:name]"', array('langcode' => $langcode)); + break; + + case 'login_notify_email_body': + $text = t("A new browser has logged in to your account. + +Details: + Account Username: [user:name] + Browser name: [browser:name] + IP Address: [browser:ip-address] + User Agent: [browser:user-agent] + +If this was you, disregard this email. + +If this wasn't you, lock this browser out here: + [browser:lock-url]", array('langcode' => $langcode)); + break; + + case 'login_notify_email_bypass_user_agent_subject': + $text = t('[site:name] login from "[browser:name]"', array('langcode' => $langcode)); + break; + + case 'login_notify_email_bypass_user_agent_body': + $text = t("An unregistered browser has logged in to your account. + +The browser had the following user agent, which is set to allow access without registering: +[browser:user-agent] + +Details: + Account Username: [user:name] + Browser name: [browser:name] + IP Address: [browser:ip-address] + User Agent: [browser:user-agent] + +If this was you, disregard this email. + +If this wasn't you, lock this browser out here: + [browser:lock-url]", array('langcode' => $langcode)); + break; + + case 'login_notify_email_blocked_subject': + $text = t('[site:name] browser "[browser:name]" blocked', array('langcode' => $langcode)); + break; + + case 'login_notify_email_blocked_body': + $text = t("A locked browser has attempted to access your account. It was blocked. + +Details: + Account Username: [user:name] + Browser name: [browser:name] + IP Address: [browser:ip-address] + User Agent: [browser:user-agent] + +If this was you, you may unlock this browser and get back into your account here: + [browser:unlock-url]", array('langcode' => $langcode)); + break; + } + } + + if ($replace) { + $text = token_replace($text, $token_data, array( + 'language' => $language, + // We do not sanitize the token replacement, since the output of this + // replacement is intended for an e-mail message, not a web browser. + 'sanitize' => FALSE, + 'clear' => TRUE, + )); + } + + return $text; +} + +/** + * Creates a secure unlock URL emailed to the user. + * + * This is similar to user_pass_reset_url() and user_pass_rehash(). + * + * @see login_notify_browser_unlock_key() + * @see user_pass_reset_url() + * @see user_pass_rehash() + */ +function login_notify_browser_unlock_url($account, $browser) { + $timestamp = REQUEST_TIME; + $key = login_notify_browser_unlock_key($account, $browser, $timestamp); + return url('user/' . $account->uid . '/browsers/browser/' . $browser['browser_id'] . '/unlock', array( + 'absolute' => TRUE, + 'query' => array( + 'timestamp' => $timestamp, + 'key' => $key, + ), + )); +} + +/** + * Creates a secure key used in the unlock URL emailed to the user. + * + * Important: the security of this is based on the assumption that the drupal + * hash salt is kept secret, as that's the only value used in the hash that + * someone using a locked browser wouldn't have access to. If that isn't a safe + * assumption then another field will have to be added to the browser table to + * store a secret value that the user would never have access to. + * + * @see login_notify_browser_unlock_url() + * @see user_pass_reset_url() + * @see user_pass_rehash() + */ +function login_notify_browser_unlock_key($account, $browser, $timestamp) { + return drupal_hmac_base64($timestamp . $browser['name'], drupal_get_hash_salt() . $browser['token']); +} + +/** + * Loads the browser record from the database with the given browser_id. + * + * 'Deleted' browsers may be returned. + * + * @param int $browser_id + * The browser_id field in the database. + * + * @return array + * The browser record from the database in an associative array. + */ +function login_notify_browser_load($browser_id) { + return db_query('SELECT * FROM {login_notify_browsers} WHERE browser_id = :browser_id', array(':browser_id' => $browser_id))->fetchAssoc(); +} + +/** + * Loads the browser record from the database with the given uid and token. + * + * 'Deleted' browsers may be returned. + * + * @param int $uid + * The UID of the user whose browser we want to load. + * @param string $token + * The browser token found in the cookie. + * + * @return array + * The browser record from the database in an associative array. + */ +function login_notify_browser_load_by_token($uid, $token) { + // Don't exclude deleted browsers when loading by token, otherwise if someone + // locked a browser and then deleted it, it wouldn't be locked anymore. + return db_query('SELECT * FROM {login_notify_browsers} WHERE uid = :uid AND token = :token', array(':uid' => $uid, ':token' => $token))->fetchAssoc(); +} + +/** + * Loads the browser record that's set to bypass the given user agent. + * + * @param int $uid + * The UID of the user whose browser we want to load. + * @param string $user_agent + * The user agent of the browser. + * + * @return array + * The browser record from the database in an associative array. + */ +function login_notify_browser_load_by_bypass_user_agent($uid, $user_agent) { + return db_query('SELECT * FROM {login_notify_browsers} WHERE uid = :uid AND bypass_user_agent = 1 AND user_agent = :user_agent AND deleted = 0', array(':uid' => $uid, ':user_agent' => $user_agent))->fetchAssoc(); +} + +/** + * Loads the browser record from the database with the given uid and name. + * + * 'Deleted' browsers will not be returned. + * + * @param int $uid + * The UID of the user whose browser we want to load. + * @param string $name + * The name of the browser. + * + * @return array + * The browser record from the database in an associative array. + */ +function login_notify_browser_load_by_name($uid, $name) { + return db_query('SELECT * FROM {login_notify_browsers} WHERE uid = :uid AND name = :name AND deleted = 0', array(':uid' => $uid, ':name' => $name))->fetchAssoc(); +} + +/** + * Saves the browser record in the database. + * + * @param array $browser + * The browser record to be saved in the database, passed in by reference. If + * 'browser_id' is not set then a new record will be inserted into the + * database and 'browser_id' will be set to the new ID. + */ +function login_notify_browser_save(&$browser) { + if (!empty($browser['user_agent']) + && strlen($browser['user_agent']) > LOGIN_NOTIFY_MAX_LENGTH_USER_AGENT) { + $browser['user_agent'] = substr($browser['user_agent'], 0, LOGIN_NOTIFY_MAX_LENGTH_USER_AGENT); + } + if (!empty($browser['browser_id'])) { + drupal_write_record('login_notify_browsers', $browser, 'browser_id'); + } + else { + // Delete any browser with the same UID and token because UID and token are + // a unique key, but if someone deletes a browser and then has to name it + // again it will use the same token. + db_delete('login_notify_browsers') + ->condition('uid', $browser['uid']) + ->condition('token', $browser['token']) + ->execute(); + + $browser['created'] = REQUEST_TIME; + drupal_write_record('login_notify_browsers', $browser); + } +} + +/** + * Retrieves all of a user's browser records from the database. + * + * 'Deleted' browsers will not be included. + * + * @param int $uid + * The UID of the user. + * + * @return array + * An array of browser records from the database, each being associative + * arrays. + */ +function login_notify_browsers_load($uid) { + $browsers = array(); + $results = db_query('SELECT * FROM {login_notify_browsers} WHERE uid = :uid AND deleted = 0 ORDER BY access DESC', array(':uid' => $uid)); + while ($browser = $results->fetchAssoc()) { + $browsers[] = $browser; + } + return $browsers; +} + +/** + * Prepares a 'destination' URL query parameter for use with drupal_goto(). + * + * This is a clone of drupal_get_destination(), except that the 'destination' + * query string parameter is ignored. + * + * @see drupal_get_destination() + */ +function login_notify_get_destination() { + $destination = &drupal_static(__FUNCTION__); + + if (isset($destination)) { + return $destination; + } + + $path = $_GET['q']; + $query = drupal_http_build_query(drupal_get_query_parameters()); + if ($query != '') { + $path .= '?' . $query; + } + $destination = array('destination' => $path); + + // Remove the 'destination' query string param because it will also override + // the drupal_goto() call. + unset($_GET['destination']); + + return $destination; +} diff --git a/sites/all/modules/contrib/security/login_notify/login_notify.pages.inc b/sites/all/modules/contrib/security/login_notify/login_notify.pages.inc new file mode 100644 index 00000000..ab01ac14 --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/login_notify.pages.inc @@ -0,0 +1,524 @@ +uid); + $current_browser = login_notify_browser_load_current($account); + + $header = array( + array('data' => t('Name'), 'nowrap' => 'nowrap'), + array('data' => t('IP Address'), 'nowrap' => 'nowrap'), + array('data' => t('User Agent'), 'nowrap' => 'nowrap'), + array('data' => t('Created'), 'nowrap' => 'nowrap'), + array('data' => t('Last Access'), 'nowrap' => 'nowrap'), + array('data' => t('Edit'), 'nowrap' => 'nowrap'), + array('data' => t('Lock'), 'nowrap' => 'nowrap'), + array('data' => t('Delete'), 'nowrap' => 'nowrap'), + ); + $rows = array(); + foreach ($browsers as $browser) { + $row = array( + 'name' => check_plain($browser['name']), + 'ip_address' => check_plain($browser['ip_address']), + 'user_agent' => check_plain($browser['user_agent']), + 'created' => array( + 'data' => date('r', $browser['created']), + 'nowrap' => 'nowrap', + ), + 'access' => array( + 'data' => date('r', $browser['access']), + 'nowrap' => 'nowrap', + ), + 'edit' => '' . t('edit') . '', + 'lock' => '' . t('lock') . '', + 'delete' => '' . t('delete') . '', + ); + if ($current_browser && $current_browser['token'] == $browser['token']) { + $row['name'] = '' . $row['name'] . ' [' . t('current') . ']'; + } + if (!empty($browser['locked'])) { + $row['name'] .= ' [' . t('locked') . ']'; + $row['lock'] = '' . t('unlock') . ''; + } + $rows[] = $row; + } + + return theme('table', array( + 'header' => $header, + 'rows' => $rows, + )); +} + +/** + * Menu form callback for user/%user/browsers/name. + * + * Asks the user to name their browser. + * + * @param object $account + * The current user. + * + * @see login_notify_user_browsers_name_form_validate() + * @see login_notify_user_browsers_name_form_submit() + */ +function login_notify_user_browsers_name_form($form, &$form_state, $account) { + $form_state['account'] = $account; + + $form = array(); + + // This will only be available if they choose a name that already exists. + // This is if, for example, it's the same browser but they lost the cookie + // somehow. Otherwise they'd have to give it a different name, go in and + // delete the old browser, and update the name of this browser. + $form['remove'] = array( + '#type' => 'checkbox', + '#title' => t('Remove the previous browser and replace it with this one'), + '#access' => FALSE, + ); + + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Browser Name'), + '#maxlength' => LOGIN_NOTIFY_MAX_LENGTH_NAME, + '#required' => TRUE, + '#description' => t('Give this device a unique nickname.'), + ); + + $form['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember me'), + '#default_value' => TRUE, + '#description' => t('Remember this browser. Uncheck to require this browser to be renamed once your browser window is closed.'), + ); + + $form['bypass_user_agent'] = array( + '#type' => 'checkbox', + '#title' => t('Do not require this or similar browsers to be named anymore'), + // Blank user agents are not allowed to bypass the browser naming process. + '#access' => !empty($_SERVER['HTTP_USER_AGENT']), + '#description' => t('This will allow browsers with the following user agent to access your account without having to give the browser a unique name, but you will still be emailed when this happens.
@useragent', array( + '@useragent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', + )), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + // See if there was an error with the name in the #validate. If there was + // then this form was rebuilt. + if (!empty($form_state['name_error'])) { + $form['remove']['#access'] = TRUE; + form_set_error('name', t('You have already used this name. Do you want to remove the previous browser?')); + // We have to add class="error" manually, it won't be done automatically by + // the above form_set_error(). + $form['name']['#attributes']['class'][] = 'error'; + } + + return $form; +} + +/** + * Form validation handler for login_notify_user_browsers_name_form(). + * + * Ensures that the user hasn't already used the browser name. + * + * @see login_notify_user_browsers_name_form() + */ +function login_notify_user_browsers_name_form_validate($form, &$form_state) { + $account = $form_state['account']; + $name = trim($form_state['values']['name']); + $remove = !empty($form_state['values']['remove']); + + if (!empty($name)) { + $current_token = NULL; + // See if there's a current (invalid) browser cookie token. + if (!empty($_COOKIE[LOGIN_NOTIFY_COOKIE_NAME])) { + $current_token = $_COOKIE[LOGIN_NOTIFY_COOKIE_NAME]; + } + // See if a browser with the same name already exists, unless they've + // checked off "remove the previous browser". + if (!$remove + && ($brower = login_notify_browser_load_by_name($account->uid, $name)) + // We're allowed to reuse the same name if we're reusing the same + // cookie token because that cookie token will be deleted from the DB + // before inserting the new one. + && $brower['token'] != $current_token) { + // Don't show the error message here because we need the form to rebuild + // so that we can show the Remove checkbox, and it won't rebuild if there + // are errors. + $form_state['name_error'] = TRUE; + } + else { + $form_state['name_error'] = FALSE; + } + } +} + +/** + * Form submit handler for login_notify_user_browsers_name_form(). + * + * Saves the browser record in the database with the chosen name, sets the + * browser cookie, and sends an email to the user notifying them that a new + * browser has accessed the site with their account. + * + * @see login_notify_user_browsers_name_form() + */ +function login_notify_user_browsers_name_form_submit($form, &$form_state) { + // Make sure there wasn't an error with the name. Since we didn't set a form + // error in the #validate it will still get to the #submit. + if (!empty($form_state['name_error'])) { + $form_state['rebuild'] = TRUE; + return; + } + + $account = $form_state['account']; + $name = trim($form_state['values']['name']); + $remember = !empty($form_state['values']['remember']); + $bypass_user_agent = !empty($form_state['values']['bypass_user_agent']); + $remove = !empty($form_state['values']['remove']); + + $token = NULL; + // See if there already is a token cookie that we can reuse (this will happen + // if someone logs into the site will multiple accounts using the same + // browser). + if (!empty($_COOKIE[LOGIN_NOTIFY_COOKIE_NAME]) + // Make sure it's the correct length. + && strlen($_COOKIE[LOGIN_NOTIFY_COOKIE_NAME]) == LOGIN_NOTIFY_TOKEN_LENGTH) { + // Reuse the same cookie token. + // We don't have to worry about the UID+token unique key here, that's + // handled by login_notify_browser_save(). + $token = $_COOKIE[LOGIN_NOTIFY_COOKIE_NAME]; + } + else { + // Create a new cookie token. + $token = user_password(LOGIN_NOTIFY_TOKEN_LENGTH); + } + + if ($remove) { + // Remove the existing browser, if there is one. + if ($existing_browser = login_notify_browser_load_by_name($account->uid, $name)) { + $existing_browser['deleted'] = TRUE; + login_notify_browser_save($existing_browser); + } + } + + $browser = array( + 'uid' => $account->uid, + 'name' => $name, + 'token' => $token, + 'ip_address' => ip_address(), + 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', + 'bypass_user_agent' => $bypass_user_agent, + 'remember' => $remember, + 'access' => REQUEST_TIME, + ); + login_notify_browser_save($browser); + + login_notify_cookie_set($account, $browser, $remember); + + drupal_set_message(t('You have named your browser %name.', array('%name' => $name))); + + // Send the email. + login_notify_browser_email_send($account, $browser); + + $form_state['rebuild'] = FALSE; +} + +/** + * Menu form callback user/%user/browsers/browser/%login_notify_browser/edit. + * + * The form that allows a user to edit a browser. + * + * @param object $account + * The user account. + * @param array $browser + * The browser record in the database. + * + * @see login_notify_user_browsers_edit_form_validate() + * @see login_notify_user_browsers_edit_form_submit() + */ +function login_notify_user_browsers_edit_form($form, &$form_state, $account, $browser) { + $form_state['account'] = $account; + $form_state['browser'] = $browser; + + $form = array(); + + $form['name'] = array( + '#type' => 'textfield', + '#title' => t('Browser Name'), + '#maxlength' => LOGIN_NOTIFY_MAX_LENGTH_NAME, + '#required' => TRUE, + '#description' => t('Give this device a unique nickname.'), + '#default_value' => $browser['name'], + ); + + $form['bypass_user_agent'] = array( + '#type' => 'checkbox', + '#title' => t('Do not require this or similar browsers to be named anymore'), + // Blank user agents are not allowed to bypass the browser naming process. + '#access' => !empty($browser['user_agent']), + '#default_value' => !empty($browser['bypass_user_agent']), + '#description' => t('This will allow browsers with the following user agent to access your account without having to give the browser a unique name, but you will still be emailed when this happens.
@useragent', array( + '@useragent' => $browser['user_agent'], + )), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + return $form; +} + +/** + * Form validation for login_notify_user_browsers_edit_form(). + * + * Ensures that they haven't already used the browser name. + * + * @see login_notify_user_browsers_edit_form() + */ +function login_notify_user_browsers_edit_form_validate($form, &$form_state) { + $browser = $form_state['browser']; + + if (!empty($form_state['values']['name'])) { + $name = trim($form_state['values']['name']); + // See if there's another browser with this name. + $name_browser = login_notify_browser_load_by_name($browser['uid'], $name); + if ($name_browser && $name_browser['browser_id'] != $browser['browser_id']) { + form_set_error('name', t('You already have another browser with this name.')); + } + } +} + +/** + * Form submit for login_notify_user_browsers_edit_form(). + * + * Saves the browser with the new name in the database. + * + * @see login_notify_user_browsers_edit_form() + */ +function login_notify_user_browsers_edit_form_submit($form, &$form_state) { + $account = $form_state['account']; + $browser = $form_state['browser']; + + $name = trim($form_state['values']['name']); + $bypass_user_agent = !empty($form_state['values']['bypass_user_agent']); + $browser['name'] = $name; + $browser['bypass_user_agent'] = $bypass_user_agent; + // Save the browser. + login_notify_browser_save($browser); + + drupal_set_message(t("You've saved the browser %name successfully.", array('%name' => $name))); + + $form_state['redirect'] = 'user/' . $account->uid . '/browsers'; +} + +/** + * Menu form callback user/%user/browsers/browser/%login_notify_browser/delete. + * + * The form that allows a user to delete a browser. + * + * @param object $account + * The user account. + * @param array $browser + * The browser record in the database. + * + * @see login_notify_user_browsers_delete_form_submit() + */ +function login_notify_user_browsers_delete_form($form, &$form_state, $account, $browser) { + $form_state['account'] = $account; + $form_state['browser'] = $browser; + + $form = array(); + + $form['message']['#markup'] = '

' . t('Are you sure you want to delete the browser %name?', array('%name' => $browser['name'])) . '

' + . '

' . t('Deleting the browser will not log them out, it will only require them to enter a new browser name.') . '

'; + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Delete browser'), + ); + + return $form; +} + +/** + * Form submit for login_notify_user_browsers_delete_form(). + * + * 'Deletes' a browser from the database, which means simply setting the + * 'deleted' field to 1, and not actually deleting a record from the table. + * + * @see login_notify_user_browsers_delete_form() + */ +function login_notify_user_browsers_delete_form_submit($form, &$form_state) { + $account = $form_state['account']; + $browser = $form_state['browser']; + + // Delete the browser. + $browser['deleted'] = TRUE; + login_notify_browser_save($browser); + + drupal_set_message(t("You've deleted the browser %name successfully.", array('%name' => $browser['name']))); + + $form_state['redirect'] = 'user/' . $account->uid . '/browsers'; +} + +/** + * Menu form callback user/%user/browsers/browser/%login_notify_browser/lock. + * + * The form that allows a user to lock a browser out of their account. + * + * @param object $account + * The user account. + * @param array $browser + * The browser record from the database. + * + * @see login_notify_user_browsers_lock_form_submit() + */ +function login_notify_user_browsers_lock_form($form, &$form_state, $account, $browser) { + $form_state['account'] = $account; + $form_state['browser'] = $browser; + + $form = array(); + + $form['message']['#markup'] = '

' . t('Are you sure you want to lock the browser %name?', array('%name' => $browser['name'])) . '

' + . '

' . t('Locking the browser will log them out and force them to log back in, requiring the username and password.') . '

'; + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Lock browser'), + ); + + return $form; +} + +/** + * Form submit handler for login_notify_user_browsers_lock_form(). + * + * Marks the browser as locked in the database. If the browser tries to access + * the site from this account again they will be forcefully logged out. + * + * Important note: this isn't completely secure. If an attacker knows that + * their browser has been or could be locked, they could delete their browser + * cookie. This would force them to name their browser again before they could + * do anything, which would send the user another notification that a new + * browser has accessed the site. + * + * For complete security, an option to prevent unrecognized browsers from + * accessing the site using your account would be required. + * + * @see login_notify_user_browsers_lock_form() + */ +function login_notify_user_browsers_lock_form_submit($form, &$form_state) { + $account = $form_state['account']; + $browser = $form_state['browser']; + + // Lock the browser. + $browser['locked'] = TRUE; + login_notify_browser_save($browser); + + drupal_set_message(t("You've locked the browser %name successfully.", array('%name' => $browser['name']))); + + $form_state['redirect'] = 'user/' . $account->uid . '/browsers'; +} + +/** + * Menu form callback user/%user/browsers/browser/%login_notify_browser/unlock. + * + * The form that allows a user to unlock a browser. + * + * @param object $account + * The user account. + * @param array $browser + * The browser record from the database. + * + * @see login_notify_user_browsers_unlock_form_submit() + */ +function login_notify_user_browsers_unlock_form($form, &$form_state, $account, $browser) { + $form_state['account'] = $account; + $form_state['browser'] = $browser; + + $form = array(); + + $form['message']['#markup'] = '

' . t('Are you sure you want to unlock the browser %name?', array('%name' => $browser['name'])) . '

' + . '

' . t('Unlocking the browser will allow it to access your account once again.') . '

'; + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Unlock browser'), + ); + + return $form; +} + +/** + * Form submit handler for login_notify_user_browsers_unlock_form(). + * + * @see login_notify_user_browsers_unlock_form() + */ +function login_notify_user_browsers_unlock_form_submit($form, &$form_state) { + $account = $form_state['account']; + $browser = $form_state['browser']; + + // Unlock the browser. + $browser['locked'] = FALSE; + login_notify_browser_save($browser); + + drupal_set_message(t("You've unlocked the browser %name successfully.", array('%name' => $browser['name']))); + + $form_state['redirect'] = 'user/' . $account->uid . '/browsers'; +} + +/** + * Handler for secure user/%user/browsers/browser/%login_notify_browser/unlock. + * + * When the above URL is called with the query parameters 'timestamp' and + * 'key' this function is called directly from our hook_init(). + * + * This unlocks a browser if the timestamp and key are correct. It works even + * if the user is logged out, which is why it's called in hook_init() rather + * than going through the menu normally. + * + * @see login_notify_init() + */ +function login_notify_user_browsers_unlock_secure($account, $browser, $timestamp, $key) { + global $user; + + $current = REQUEST_TIME; + // Check the timestamp, it must be less than X seconds ago. + if ($timestamp < $current && $timestamp >= $current - LOGIN_NOTIFY_UNLOCK_TIMEOUT + // Make sure the key is valid. + && $key == login_notify_browser_unlock_key($account, $browser, $timestamp)) { + // Valid unlock URL. + // Unlock the browser. + $browser['locked'] = FALSE; + login_notify_browser_save($browser); + + drupal_set_message(t("You've unlocked the browser %name successfully.", array('%name' => $browser['name']))); + + // If they're not logged in, redirect to the login page. + if (empty($user->uid)) { + drupal_goto('user'); + } + // Otherwise, redirect to the browsers page. + else { + drupal_goto('user/' . $user->uid . '/browsers'); + } + } + else { + drupal_set_message(t('Browser unlock URL is invalid.'), 'error'); + } +} diff --git a/sites/all/modules/contrib/security/login_notify/login_notify.tokens.inc b/sites/all/modules/contrib/security/login_notify/login_notify.tokens.inc new file mode 100644 index 00000000..bdd0c799 --- /dev/null +++ b/sites/all/modules/contrib/security/login_notify/login_notify.tokens.inc @@ -0,0 +1,86 @@ + array( + 'browser' => array( + 'name' => t('Browser'), + 'description' => t("The user's registered browser."), + 'needs-data' => 'browser', + ), + ), + 'tokens' => array( + 'browser' => array( + 'name' => array( + 'name' => t('Name'), + 'description' => t('The unique name assigned to the browser.'), + ), + 'ip-address' => array( + 'name' => t('IP Address'), + 'description' => t('The IP address that registered the browser.'), + ), + 'user-agent' => array( + 'name' => t('User Agent'), + 'description' => t('The user agent that registered the browser.'), + ), + 'lock-url' => array( + 'name' => t('Lock URL'), + 'description' => t('The URL to lock the browser.'), + ), + 'unlock-url' => array( + 'name' => t('Unock URL'), + 'description' => t('The URL to unlock the browser.'), + ), + ), + ), + ); +} + +/** + * Implements hook_tokens(). + */ +function login_notify_tokens($type, $tokens, array $data = array(), array $options = array()) { + $sanitize = !empty($options['sanitize']); + + $replacements = array(); + + if ($type == 'browser' && !empty($data['browser'])) { + $browser = $data['browser']; + foreach ($tokens as $name => $original) { + switch ($name) { + case 'name': + $replacements[$original] = $sanitize ? check_plain($browser['name']) : $browser['name']; + break; + + case 'ip-address': + $replacements[$original] = $sanitize ? check_plain($browser['ip_address']) : $browser['ip_address']; + break; + + case 'user-agent': + $replacements[$original] = $sanitize ? check_plain($browser['user_agent']) : $browser['user_agent']; + break; + + case 'lock-url': + $url = url('user/' . $browser['uid'] . '/browsers/browser/' . $browser['browser_id'] . '/lock', array('absolute' => TRUE)); + $replacements[$original] = $sanitize ? check_plain($url) : $url; + break; + + case 'unlock-url': + $account = user_load($browser['uid']); + $url = login_notify_browser_unlock_url($account, $browser); + $replacements[$original] = $sanitize ? check_plain($url) : $url; + break; + } + } + } + + return $replacements; +}