From 0e730eca64ab1fc46c4eb998734926b41bd508b2 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Wed, 26 Jul 2017 13:41:35 +0200 Subject: [PATCH] New module 'Token' --- sites/all/modules/token/API.txt | 132 +++ sites/all/modules/token/LICENSE.txt | 339 +++++++ sites/all/modules/token/README.txt | 30 + sites/all/modules/token/arrow-down.png | Bin 0 -> 118 bytes sites/all/modules/token/arrow-right.png | Bin 0 -> 127 bytes sites/all/modules/token/jquery.treeTable.css | 44 + sites/all/modules/token/jquery.treeTable.js | 220 +++++ sites/all/modules/token/token.api.php | 100 ++ sites/all/modules/token/token.css | 23 + sites/all/modules/token/token.info | 10 + sites/all/modules/token/token.install | 43 + sites/all/modules/token/token.js | 51 + sites/all/modules/token/token.module | 960 +++++++++++++++++++ sites/all/modules/token/token.pages.inc | 158 +++ sites/all/modules/token/token.rules.inc | 120 +++ sites/all/modules/token/token.test | 690 +++++++++++++ sites/all/modules/token/tokenSTARTER.info | 11 + sites/all/modules/token/tokenSTARTER.module | 68 ++ sites/all/modules/token/token_actions.info | 11 + sites/all/modules/token/token_actions.module | 300 ++++++ sites/all/modules/token/token_actions.test | 79 ++ sites/all/modules/token/token_comment.inc | 91 ++ sites/all/modules/token/token_node.inc | 328 +++++++ sites/all/modules/token/token_taxonomy.inc | 81 ++ sites/all/modules/token/token_user.inc | 78 ++ 25 files changed, 3967 insertions(+) create mode 100644 sites/all/modules/token/API.txt create mode 100644 sites/all/modules/token/LICENSE.txt create mode 100644 sites/all/modules/token/README.txt create mode 100644 sites/all/modules/token/arrow-down.png create mode 100644 sites/all/modules/token/arrow-right.png create mode 100644 sites/all/modules/token/jquery.treeTable.css create mode 100644 sites/all/modules/token/jquery.treeTable.js create mode 100644 sites/all/modules/token/token.api.php create mode 100644 sites/all/modules/token/token.css create mode 100644 sites/all/modules/token/token.info create mode 100644 sites/all/modules/token/token.install create mode 100644 sites/all/modules/token/token.js create mode 100644 sites/all/modules/token/token.module create mode 100644 sites/all/modules/token/token.pages.inc create mode 100644 sites/all/modules/token/token.rules.inc create mode 100644 sites/all/modules/token/token.test create mode 100644 sites/all/modules/token/tokenSTARTER.info create mode 100644 sites/all/modules/token/tokenSTARTER.module create mode 100644 sites/all/modules/token/token_actions.info create mode 100644 sites/all/modules/token/token_actions.module create mode 100644 sites/all/modules/token/token_actions.test create mode 100644 sites/all/modules/token/token_comment.inc create mode 100644 sites/all/modules/token/token_node.inc create mode 100644 sites/all/modules/token/token_taxonomy.inc create mode 100644 sites/all/modules/token/token_user.inc diff --git a/sites/all/modules/token/API.txt b/sites/all/modules/token/API.txt new file mode 100644 index 0000000..de57ec7 --- /dev/null +++ b/sites/all/modules/token/API.txt @@ -0,0 +1,132 @@ + +Overview +======== +In many cases, it's useful to allow users to define patterns or large +chunks of text that contain programmatically derived values. For example, +form email messages addressed to a given user, or url path aliases +containing the title of a given node. Both examples require bits of data +that vary each time the text is generated -- node titles, user ids, and +so on. Rather than forcing users to embed ugly snippets of PHP, or creating +elaborate and bizarre UIs for configuring the patterns via the browser, +it's most useful to give users a set of 'placeholder' tokens to place in +their text. + +Token.module provides a shared API for exposing and using placeholder +tokens and their appropriate replacement values. It does nothing *by +itself* -- other modules can use it to avoid reinventing the wheel. + +Using Token Replacement +======================= +To apply token replacement to a chunk of text, you have two options. The +first, and simplest, is: + + token_replace($original, $type = 'global', $object = NULL, + $leading = '[', $trailing = ']') + +$original is the source text to perform substitutions on: it can be either +a simple string, or an array of multiple strings. + +$type and $object are to be used when performing substitution based on a +particular Drupal object. Replacing tokens in an email with values from +a particular user account, or replacing tokens in a path alias pattern with +values from the node being aliased, are two examples. + +$type should contain the general object type (node, comment, user, etc.) +while $object should contain the object itself. + +$leading and $trailing can be used to override the default token style. +For example, to replace tokens using %this style, pass in '%' and '' for +the $leading and $trailing values. Note that passing in a leading but NOT +trailing value can lead to false positives if two tokens are named in a +similar fashion (%node_term and %node_term_id, for example). + + + +Altering The Replacement Values +=============================== +If your module needs to perform additional cleanup work on the replacement +values before doing the actual substitutions (cleaning replacement values +to make them appropriate for path aliasing, truncating them to a particular +length, etc.) you can manually retrieve the list of tokens and replacement +values, then call str_replace() yourself. + + token_get_values($type = 'global', $object = NULL) + +Pass in the $type and $object as you would with the simpler token_replace() +function. The return value will be an object containing one array of tokens +and one array of values as in this example: + +stdClass Object { + [tokens] => array( + [0] => mytoken1, + [1] => mytoken2 + ), + [values] => array( + [0] => value1, + [1] => value2, + ) +} + + + +Providing Placeholder Tokens +============================ +Token.module provides a small set of default placeholders for global values +like the name of the currently logged in user, the site's URL, and so on. +Any module can provide additional tokens by implementing two hooks. + +Security note: For tokens which include user input, users and modules +expect to see both a ['token-name'] and a ['token-name-raw'] value. + + +hook_token_values($type, $object = NULL) +======================================== +This function should return a keyed array of placeholders, and their +replacement values. $type contains the current context -- 'node', 'user', +'global', etc. $object contains the specific node, user, etc. that +should be used as the basis for the replacements. *Only* generate and +return replacement tokens when $type is something that your module can +really deal with. That helps keep things speedy and avoid needlessly +searching for jillions of replacement tokens. The $options array can +contain additional options (exact use is dynamic and not easily documented). + +For example: + +function my_user_token_values($type, $object = NULL, $options = array()) { + if ($type == 'user') { + $user = $object; + $tokens['name'] = $user->name; + $tokens['mail'] = $user->mail; + return $tokens; + } +} + + +hook_token_list($type = 'all') +============================== +This function is used to provide help and inline documentation for all +of the possible replacement tokens. + +As with hook_token_values, $type indicates the context that token help +is being generated for. Unlike hook_token_values however, you should +show ALL tokens at the same time if $type is 'all'. As such, the help +text should be keyed by the $type context your module will use when +doing the actual replacements. For example: + +function my_user_token_list($type = 'all') { + if ($type == 'user' || $type == 'all') { + $tokens['user']['name'] = t("The user's name"); + $tokens['user']['mail'] = t("The user's email address"); + return $tokens; + } +} + +Examples of more elaborate token replacement setups can be found in the +token_node.inc file that's bundled with token.module. + +Security Note +======== +If use any of the tokens in the ['raw'] sub-array then please note that these +are unfiltered values which could conceivably contain XSS attacks or other +malicious data. Your module should then provide it's own filtering to ensure the +safety of site users. diff --git a/sites/all/modules/token/LICENSE.txt b/sites/all/modules/token/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/token/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/token/README.txt b/sites/all/modules/token/README.txt new file mode 100644 index 0000000..30c52cd --- /dev/null +++ b/sites/all/modules/token/README.txt @@ -0,0 +1,30 @@ + +Description +=========== +Token module provides a centralized API for text substitution. Unless +you're installing another Drupal module that requires it, this software +is probably unnecessary. + +For more information on tokens see http://groups.drupal.org/tokens + +Benefits +======== +If you're a Drupal developer, check out API.txt for detailed instructions +on using the Token API. It allows every module to announce the placeholder +tokens they can handle, uses simple caching to prevent duplicated work in +a given page-view, and is pretty lightweight. It's nice. You'll like it. + +tokenSTARTER +============ +Want to add your own custom tokens to a site? Not sure how to write a +module? Worry no more, it's now quite easy. + +1. Copy and rename the tokenSTARTER.info and tokenSTARTER.module replacing + every instance of STARTER with a descriptive, appropriate word. + +2. Edit the .module file and change hook_token_list and hook_token_values + to provide whatever additional tokens or logic your site needs. + +3. Enable the module and enjoy! + +You should also want to read the API.txt. \ No newline at end of file diff --git a/sites/all/modules/token/arrow-down.png b/sites/all/modules/token/arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..2edbb17d7768f01251d9862ae4c0d093b2e402fc GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4@NBt2Bi?12_!@pBj@Sj7*a7OIl+PTtGvV;d4m)32@moU64DbAnz)!5ysKE)Jent;2dZT7 MboFyt=akR{0H9+ap#T5? literal 0 HcmV?d00001 diff --git a/sites/all/modules/token/arrow-right.png b/sites/all/modules/token/arrow-right.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2f48238bf9cf15d2a83eda46935f2ceaac504a GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2?f#ZI0f96(URkTSX|qs(874R X{@=v?*S^05n!w=c>gTe~DWM4f{CF)b literal 0 HcmV?d00001 diff --git a/sites/all/modules/token/jquery.treeTable.css b/sites/all/modules/token/jquery.treeTable.css new file mode 100644 index 0000000..bec225a --- /dev/null +++ b/sites/all/modules/token/jquery.treeTable.css @@ -0,0 +1,44 @@ + +/* jQuery TreeTable Core 2.0 stylesheet + * + * This file contains styles that are used to display the tree table. Each tree + * table is assigned the +treeTable+ class. + * ========================================================================= */ + +/* jquery.treeTable.collapsible + * ------------------------------------------------------------------------- */ +.treeTable tr td .expander { + background-position: left center; + background-repeat: no-repeat; + cursor: pointer; + padding: 0; + zoom: 1; /* IE7 Hack */ +} + +.treeTable tr.collapsed td .expander { + background-image: url(arrow-right.png); +} + +.treeTable tr.expanded td .expander { + background-image: url(arrow-down.png); +} + +/* jquery.treeTable.sortable + * ------------------------------------------------------------------------- */ +.treeTable tr.selected, .treeTable tr.accept { + background-color: #3875d7; + color: #fff; +} + +.treeTable tr.collapsed.selected td .expander, .treeTable tr.collapsed.accept td .expander { + background-image: url(../images/toggle-expand-light.png); +} + +.treeTable tr.expanded.selected td .expander, .treeTable tr.expanded.accept td .expander { + background-image: url(../images/toggle-collapse-light.png); +} + +.treeTable .ui-draggable-dragging { + color: #000; + z-index: 1; +} diff --git a/sites/all/modules/token/jquery.treeTable.js b/sites/all/modules/token/jquery.treeTable.js new file mode 100644 index 0000000..847b8f3 --- /dev/null +++ b/sites/all/modules/token/jquery.treeTable.js @@ -0,0 +1,220 @@ + +/* + * jQuery treeTable Plugin 2.3.0 + * http://ludo.cubicphuse.nl/jquery-plugins/treeTable/ + * + * Copyright 2010, Ludo van den Boom + * Dual licensed under the MIT or GPL Version 2 licenses. + */ +(function($) { + // Helps to make options available to all functions + // TODO: This gives problems when there are both expandable and non-expandable + // trees on a page. The options shouldn't be global to all these instances! + var options; + var defaultPaddingLeft; + + $.fn.treeTable = function(opts) { + options = $.extend({}, $.fn.treeTable.defaults, opts); + + return this.each(function() { + $(this).addClass("treeTable").find("tbody tr").each(function() { + // Initialize root nodes only if possible + if(!options.expandable || $(this)[0].className.search(options.childPrefix) == -1) { + // To optimize performance of indentation, I retrieve the padding-left + // value of the first root node. This way I only have to call +css+ + // once. + if (isNaN(defaultPaddingLeft)) { + defaultPaddingLeft = parseInt($($(this).children("td")[options.treeColumn]).css('padding-left'), 10); + } + + initialize($(this)); + } else if(options.initialState == "collapsed") { + this.style.display = "none"; // Performance! $(this).hide() is slow... + } + }); + }); + }; + + $.fn.treeTable.defaults = { + childPrefix: "child-of-", + clickableNodeNames: false, + expandable: true, + indent: 19, + initialState: "collapsed", + treeColumn: 0 + }; + + // Recursively hide all node's children in a tree + $.fn.collapse = function() { + $(this).addClass("collapsed"); + + childrenOf($(this)).each(function() { + if(!$(this).hasClass("collapsed")) { + $(this).collapse(); + } + + this.style.display = "none"; // Performance! $(this).hide() is slow... + }); + + return this; + }; + + // Recursively show all node's children in a tree + $.fn.expand = function() { + $(this).removeClass("collapsed").addClass("expanded"); + + childrenOf($(this)).each(function() { + initialize($(this)); + + if($(this).is(".expanded.parent")) { + $(this).expand(); + } + + // this.style.display = "table-row"; // Unfortunately this is not possible with IE :-( + $(this).show(); + }); + + return this; + }; + + // Reveal a node by expanding all ancestors + $.fn.reveal = function() { + $(ancestorsOf($(this)).reverse()).each(function() { + initialize($(this)); + $(this).expand().show(); + }); + + return this; + }; + + // Add an entire branch to +destination+ + $.fn.appendBranchTo = function(destination) { + var node = $(this); + var parent = parentOf(node); + + var ancestorNames = $.map(ancestorsOf($(destination)), function(a) { return a.id; }); + + // Conditions: + // 1: +node+ should not be inserted in a location in a branch if this would + // result in +node+ being an ancestor of itself. + // 2: +node+ should not have a parent OR the destination should not be the + // same as +node+'s current parent (this last condition prevents +node+ + // from being moved to the same location where it already is). + // 3: +node+ should not be inserted as a child of +node+ itself. + if($.inArray(node[0].id, ancestorNames) == -1 && (!parent || (destination.id != parent[0].id)) && destination.id != node[0].id) { + indent(node, ancestorsOf(node).length * options.indent * -1); // Remove indentation + + if(parent) { node.removeClass(options.childPrefix + parent[0].id); } + + node.addClass(options.childPrefix + destination.id); + move(node, destination); // Recursively move nodes to new location + indent(node, ancestorsOf(node).length * options.indent); + } + + return this; + }; + + // Add reverse() function from JS Arrays + $.fn.reverse = function() { + return this.pushStack(this.get().reverse(), arguments); + }; + + // Toggle an entire branch + $.fn.toggleBranch = function() { + if($(this).hasClass("collapsed")) { + $(this).expand(); + } else { + $(this).removeClass("expanded").collapse(); + } + + return this; + }; + + // === Private functions + + function ancestorsOf(node) { + var ancestors = []; + while(node = parentOf(node)) { + ancestors[ancestors.length] = node[0]; + } + return ancestors; + }; + + function childrenOf(node) { + return $(node).siblings("tr." + options.childPrefix + node[0].id); + }; + + function getPaddingLeft(node) { + var paddingLeft = parseInt(node[0].style.paddingLeft, 10); + return (isNaN(paddingLeft)) ? defaultPaddingLeft : paddingLeft; + } + + function indent(node, value) { + var cell = $(node.children("td")[options.treeColumn]); + cell[0].style.paddingLeft = getPaddingLeft(cell) + value + "px"; + + childrenOf(node).each(function() { + indent($(this), value); + }); + }; + + function initialize(node) { + if(!node.hasClass("initialized")) { + node.addClass("initialized"); + + var childNodes = childrenOf(node); + + if(!node.hasClass("parent") && childNodes.length > 0) { + node.addClass("parent"); + } + + if(node.hasClass("parent")) { + var cell = $(node.children("td")[options.treeColumn]); + var padding = getPaddingLeft(cell) + options.indent; + + childNodes.each(function() { + $(this).children("td")[options.treeColumn].style.paddingLeft = padding + "px"; + }); + + if(options.expandable) { + cell.prepend(''); + $(cell[0].firstChild).click(function() { node.toggleBranch(); }); + + if(options.clickableNodeNames) { + cell[0].style.cursor = "pointer"; + $(cell).click(function(e) { + // Don't double-toggle if the click is on the existing expander icon + if (e.target.className != 'expander') { + node.toggleBranch(); + } + }); + } + + // Check for a class set explicitly by the user, otherwise set the default class + if(!(node.hasClass("expanded") || node.hasClass("collapsed"))) { + node.addClass(options.initialState); + } + + if(node.hasClass("expanded")) { + node.expand(); + } + } + } + } + }; + + function move(node, destination) { + node.insertAfter(destination); + childrenOf(node).reverse().each(function() { move($(this), node[0]); }); + }; + + function parentOf(node) { + var classNames = node[0].className.split(' '); + + for(key in classNames) { + if(classNames[key].match(options.childPrefix)) { + return $(node).siblings("#" + classNames[key].substring(options.childPrefix.length)); + } + } + }; +})(jQuery); diff --git a/sites/all/modules/token/token.api.php b/sites/all/modules/token/token.api.php new file mode 100644 index 0000000..86236c7 --- /dev/null +++ b/sites/all/modules/token/token.api.php @@ -0,0 +1,100 @@ +nid); + } + + return $values; +} + +/** + * Alter replacement values for placeholder tokens. + * + * @param $replacements + * An associative array of replacements returned by hook_token_values(). + * @param $context + * The context in which hook_token_values() was called. An associative array + * with the following keys, which have the same meaning as the corresponding + * parameters of hook_token_values(): + * - 'type' + * - 'object' + * - 'options' + * + * @see hook_token_values() + */ +function hook_token_values_alter(&$replacements, $context) { + if ($context['type'] == 'node' && !empty($context['object'])) { + $node = $context['object']; + + if (isset($replacements['title-raw']) && !empty($node->field_title[0]['value'])) { + $title = $node->field_title[0]['value']; + $replacements['title-raw'] = $title; + $replacements['title'] = check_plain($title); + } + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/sites/all/modules/token/token.css b/sites/all/modules/token/token.css new file mode 100644 index 0000000..5c3b3ba --- /dev/null +++ b/sites/all/modules/token/token.css @@ -0,0 +1,23 @@ + +.token-tree { + font-size: 0.85em; + margin-left: 19px; +} + +.token-tree td, .token-tree th { + padding-top: 0; + padding-bottom: 0; +} + +.token-group { + font-weight: bold; +} + +.js .token-group { + font-weight: normal; +} + +/* Prevent the token columns from being wrapped. */ +.token-tree td.token-key { + white-space: nowrap; +} diff --git a/sites/all/modules/token/token.info b/sites/all/modules/token/token.info new file mode 100644 index 0000000..f88e9d5 --- /dev/null +++ b/sites/all/modules/token/token.info @@ -0,0 +1,10 @@ +name = Token +description = Provides a shared API for replacement of textual placeholders with actual data. +core = 6.x + +; Information added by drupal.org packaging script on 2012-09-12 +version = "6.x-1.19" +core = "6.x" +project = "token" +datestamp = "1347470077" + diff --git a/sites/all/modules/token/token.install b/sites/all/modules/token/token.install new file mode 100644 index 0000000..525391b --- /dev/null +++ b/sites/all/modules/token/token.install @@ -0,0 +1,43 @@ + $modules) { + $duplicate_tokens[$token] = $t('@token (defined by modules: @modules)', array('@token' => $token, '@modules' => implode(', ', $modules))); + } + $requirements['token_duplicates'] = array( + 'title' => $t('Duplicate tokens'), + 'value' => $t('The following tokens are defined by multiple modules and may cause problems when performing token replacement.'), + 'severity' => REQUIREMENT_WARNING, + 'description' => theme('item_list', $duplicate_tokens), + ); + } + } + + return $requirements; +} + +function token_install() { + db_query("UPDATE {system} SET weight = 10 WHERE name = 'token'"); +} + +function token_update_1() { + $ret = array(); + $ret[] = update_sql("UPDATE {system} SET weight = 10 WHERE name = 'token'"); + return $ret; +} diff --git a/sites/all/modules/token/token.js b/sites/all/modules/token/token.js new file mode 100644 index 0000000..a1f0c4c --- /dev/null +++ b/sites/all/modules/token/token.js @@ -0,0 +1,51 @@ + +(function ($) { + +Drupal.behaviors.tokenTree = function() { + $('table.token-tree').each(function() { + $(this).treeTable(); + }); +}; + +Drupal.behaviors.tokenInsert = function() { + // Keep track of which textfield was last selected/focused. + $('textarea, input[type="text"]').focus(function() { + Drupal.settings.tokenFocusedField = this; + }); + + $('.token-click-insert .token-key').each(function() { + var newThis = $('' + $(this).html() + '').click(function(){ + if (typeof Drupal.settings.tokenFocusedField == 'undefined') { + alert(Drupal.t('First click a text field to insert your tokens into.')); + } + else { + var myField = Drupal.settings.tokenFocusedField; + var myValue = $(this).text(); + + //IE support + if (document.selection) { + myField.focus(); + sel = document.selection.createRange(); + sel.text = myValue; + } + + //MOZILLA/NETSCAPE support + else if (myField.selectionStart || myField.selectionStart == '0') { + var startPos = myField.selectionStart; + var endPos = myField.selectionEnd; + myField.value = myField.value.substring(0, startPos) + + myValue + + myField.value.substring(endPos, myField.value.length); + } else { + myField.value += myValue; + } + + $('html,body').animate({scrollTop: $(myField).offset().top}, 500); + } + return false; + }); + $(this).html(newThis); + }); +}; + +})(jQuery); diff --git a/sites/all/modules/token/token.module b/sites/all/modules/token/token.module new file mode 100644 index 0000000..faf1db2 --- /dev/null +++ b/sites/all/modules/token/token.module @@ -0,0 +1,960 @@ +'; + $output .= '
' . t('List of the currently available tokens on this site') . '
'; + $output .= '
' . theme('token_tree', 'all', TRUE, FALSE) . '
'; + $output .= ''; + return $output; + } +} + +/** + * Return an array of the core modules supported by token.module. + */ +function _token_core_supported_modules() { + return array('node', 'user', 'taxonomy', 'comment', 'menu', 'book'); +} + +/** + * Implements hook_menu(). + */ +function token_menu() { + $items = array(); + + // Devel token pages. + if (module_exists('devel')) { + $items['node/%node/devel/token'] = array( + 'title' => 'Tokens', + 'page callback' => 'token_devel_token_object', + 'page arguments' => array('node', 1), + 'access arguments' => array('access devel information'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'token.pages.inc', + 'weight' => 5, + ); + $items['user/%user/devel/token'] = array( + 'title' => 'Tokens', + 'page callback' => 'token_devel_token_object', + 'page arguments' => array('user', 1), + 'access arguments' => array('access devel information'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'token.pages.inc', + 'weight' => 5, + ); + } + + return $items; +} + +/** + * Implements hook_theme(). + */ +function token_theme() { + return array( + 'token_help' => array( + 'arguments' => array('type' => 'all', 'prefix' => TOKEN_PREFIX, 'suffix' => TOKEN_SUFFIX), + 'file' => 'token.pages.inc', + ), + 'token_tree' => array( + 'arguments' => array('token_types' => array(), 'global_types' => TRUE , 'click_insert' => TRUE), + 'file' => 'token.pages.inc', + ), + ); +} + +/** + * Implements hook_token_values(). + */ +function token_token_values($type, $object = NULL) { + global $user; + $values = array(); + + switch ($type) { + case 'global': + // Current user tokens. + $values['user-name'] = $user->uid ? $user->name : variable_get('anonymous', t('Anonymous')); + $values['user-id'] = $user->uid ? $user->uid : 0; + $values['user-mail'] = $user->uid ? $user->mail : ''; + + // Site information tokens. + $values['site-url'] = url('', array('absolute' => TRUE)); + $values['site-name'] = check_plain(variable_get('site_name', t('Drupal'))); + $values['site-slogan'] = check_plain(variable_get('site_slogan', '')); + $values['site-mission'] = filter_xss_admin(variable_get('site_mission', '')); + $values['site-mail'] = variable_get('site_mail', ''); + $values += token_get_date_token_values(NULL, 'site-date-'); + + // Current page tokens. + $values['current-page-title'] = drupal_get_title(); + $alias = drupal_get_path_alias($_GET['q']); + $values['current-page-path-raw'] = $alias; + $values['current-page-path'] = check_plain($alias); + $values['current-page-url'] = url($_GET['q'], array('absolute' => TRUE)); + + $page = isset($_GET['page']) ? $_GET['page'] : ''; + $pager_page_array = explode(',', $page); + $page = $pager_page_array[0]; + $values['current-page-number'] = (int) $page + 1; + + // Backwards compatability for renamed tokens. + $values['site-date'] = $values['site-date-small']; + $values['page-number'] = $values['current-page-number']; + + break; + } + return $values; +} + +/** + * Implements hook_token_list(). + */ +function token_token_list($type = 'all') { + $tokens = array(); + + if ($type == 'global' || $type == 'all') { + // Current user tokens. + $tokens['global']['user-name'] = t('The name of the currently logged in user.'); + $tokens['global']['user-id'] = t('The user ID of the currently logged in user.'); + $tokens['global']['user-mail'] = t('The email address of the currently logged in user.'); + + // Site information tokens. + $tokens['global']['site-url'] = t("The URL of the site's front page."); + $tokens['global']['site-name'] = t('The name of the site.'); + $tokens['global']['site-slogan'] = t('The slogan of the site.'); + $tokens['global']['site-mission'] = t("The optional 'mission' of the site."); + $tokens['global']['site-mail'] = t('The administrative email address for the site.'); + $tokens['global'] += token_get_date_token_info(t('The current'), 'site-date-'); + + // Current page tokens. + $tokens['global']['current-page-title'] = t('The title of the current page.'); + $tokens['global']['current-page-path'] = t('The URL alias of the current page.'); + $tokens['global']['current-page-path-raw'] = t('The URL alias of the current page.'); + $tokens['global']['current-page-url'] = t('The URL of the current page.'); + $tokens['global']['current-page-number'] = t('The page number of the current page when viewing paged lists.'); + } + + return $tokens; +} + +/** + * General function to include the files that token relies on for the real work. + */ +function token_include() { + static $run = FALSE; + + if (!$run) { + $run = TRUE; + $modules_enabled = array_keys(module_list()); + $modules = array_intersect(_token_core_supported_modules(), $modules_enabled); + foreach ($modules as $module) { + module_load_include('inc', 'token', "token_$module"); + } + } +} + +/** + * Replace all tokens in a given string with appropriate values. + * + * @param $text + * A string potentially containing replaceable tokens. + * @param $type + * (optional) A flag indicating the class of substitution tokens to use. If + * an object is passed in the second param, 'type' should contain the + * object's type. For example, 'node', 'comment', or 'user'. If no type is + * specified, only 'global' site-wide substitution tokens are built. + * @param $object + * (optional) An object to use for building substitution values (e.g. a node + * comment, or user object). + * @param $leading + * (optional) Character(s) to prepend to the token key before searching for + * matches. Defaults to TOKEN_PREFIX. + * @param $trailing + * (optional) Character(s) to append to the token key before searching for + * matches. Defaults to TOKEN_SUFFIX. + * @param $options + * (optional) A keyed array of settings and flags to control the token + * generation and replacement process. Supported options are: + * - clear: A boolean flag indicating that tokens should be removed from the + * final text if no replacement value can be generated. + * @param $flush + * (optional) A flag indicating whether or not to flush the token cache. + * Useful for processes that need to slog through huge numbers of tokens + * in a single execution cycle. Flushing it will keep them from burning + * through memory. Defaults to FALSE. + * + * @return + * Text with tokens replaced. + */ +function token_replace($text, $type = 'global', $object = NULL, $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX, $options = array(), $flush = FALSE) { + return token_replace_multiple($text, array($type => $object), $leading, $trailing, $options, $flush); +} + +/** + * Replace all tokens in a given string with appropriate values. + * + * Contrary to token_replace() this function supports replacing multiple types. + * + * @param $text + * A string potentially containing replaceable tokens. + * @param $types + * (optional) An array of substitution classes and optional objects. The key + * is a flag indicating the class of substitution tokens to use. If an object + * is passed as value, the key should contain the object's type. For example, + * 'node', 'comment', or 'user'. The object will be used for building + * substitution values. If no type is specified, only 'global' site-wide + * substitution tokens are built. + * @param $leading + * (optional) Character(s) to prepend to the token key before searching for + * matches. Defaults to TOKEN_PREFIX. + * @param $trailing + * (optional) Character(s) to append to the token key before searching for + * matches. Defaults to TOKEN_SUFFIX. + * @param $options + * (optional) A keyed array of settings and flags to control the token + * generation and replacement process. Supported options are: + * - clear: A boolean flag indicating that tokens should be removed from the + * final text if no replacement value can be generated. + * @param $flush + * (optional) A flag indicating whether or not to flush the token cache. + * Useful for processes that need to slog through huge numbers of tokens + * in a single execution cycle. Flushing it will keep them from burning + * through memory. Defaults to FALSE. + * + * @return + * Text with tokens replaced. + */ +function token_replace_multiple($text, $types = array('global' => NULL), $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX, $options = array(), $flush = FALSE) { + // Ensure that the $text parameter is a string and not an array which is an + // invalid input. + if (is_array($text)) { + foreach ($text as $key => $value) { + $text[$key] = token_replace_multiple($value, $types, $leading, $trailing, $options, $flush); + } + return $text; + } + + // If there are no tokens to replace, just return the text. + $text_tokens = token_scan($text, $leading, $trailing); + if (empty($text_tokens)) { + return $text; + } + + $full = new stdClass(); + $full->tokens = $full->values = array(); + + // Allow global token replacement by default. + if (empty($types) || !is_array($types)) { + $types = array('global' => NULL); + } + + foreach ($types as $type => $object) { + $temp = token_get_values($type, $object, $flush, $options); + $full->tokens = array_merge($full->tokens, $temp->tokens); + $full->values = array_merge($full->values, $temp->values); + } + + // Support clearing out tokens that would not be replaced. + if (!empty($options['clear'])) { + foreach ($text_tokens as $token) { + if (!in_array($token, $full->tokens)) { + $full->tokens[] = $token; + $full->values[] = ''; + } + } + } + + $tokens = token_prepare_tokens($full->tokens, $leading, $trailing); + return str_replace($tokens, $full->values, $text); +} + +/** + * Return a list of valid substitution tokens and their values for + * the specified type. + * + * @param $type + * (optional) A flag indicating the class of substitution tokens to use. If an + * object is passed in the second param, 'type' should contain the + * object's type. For example, 'node', 'comment', or 'user'. If no + * type is specified, only 'global' site-wide substitution tokens are + * built. + * @param $object + * (optional) An object to use for building substitution values (e.g. a node + * comment, or user object). + * @param $flush + * (optional) A flag indicating whether or not to flush the token cache. + * Useful for processes that need to slog through huge numbers of tokens + * in a single execution cycle. Flushing it will keep them from burning + * through memory. Defaults to FALSE. + * @param $options + * (optional) A keyed array of settings and flags to control the token + * generation process. + * + * @return + * An object with two properties: + * - tokens: All the possible tokens names generated. + * - values: The corresponding values for the tokens. + * + * Note that before performing actual token replacement that the token names + * should be run through token_prepare_tokens(). + */ +function token_get_values($type = 'global', $object = NULL, $flush = FALSE, $options = array()) { + static $tokens = array(); + static $running = FALSE; + + // Simple recursion check. This is to avoid content_view()'s potential + // for endless looping when a filter uses tokens, which load the content + // view, which calls the filter, which uses tokens, which... + if ($running) { + // We'll allow things to get two levels deep, but bail out after that + // without performing any substitutions. + $result = new stdClass(); + $result->tokens = array(); + $result->values = array(); + return $result; + } + else { + $running = TRUE; + } + + // Flush the static token cache. Useful for processes that need to slog + // through huge numbers of tokens in a single execution cycle. Flushing it + // will keep them from burning through memory. + if ($flush || !empty($options['reset'])) { + $tokens = array(); + } + + // Allow simple resets of the static values. + if ($type === 'reset') { + $tokens = array(); + $running = FALSE; + return; + } + + // Neutralize options that do not affect token replacement. + $serialized_options = $options; + unset($serialized_options['clear']); + + // Store the token cache by object ID and serialized options. + $cid = _token_get_id($type, $object) . ':' . md5(serialize($serialized_options)); + if ($type != 'global' && !isset($tokens[$type][$cid])) { + token_include(); + $tokens[$type][$cid] = module_invoke_all('token_values', $type, $object, $options); + } + + // Special-case global tokens, as we always want to be able to process + // those substitutions. + if (!isset($tokens['global'][$cid])) { + token_include(); + $tokens['global'][$cid] = module_invoke_all('token_values', 'global', NULL, $options); + } + + $all = $tokens['global'][$cid]; + if ($type != 'global') { + // Avoid using array_merge() if only global tokens were requested. + $all = array_merge($all, $tokens[$type][$cid]); + } + + // Allow other modules to alter the replacements. + $context = array( + 'type' => $type, + 'object' => $object, + 'options' => $options, + ); + drupal_alter('token_values', $all, $context); + + $result = new stdClass(); + $result->tokens = array_keys($all); + $result->values = array_values($all); + + $running = FALSE; + + return $result; +} + +/** + * A helper function that retrieves all currently exposed tokens, + * and merges them recursively. This is only necessary when building + * the token listing -- during actual value replacement, only tokens + * in a particular domain are requested and a normal array_marge() is + * sufficient. + * + * @param $types + * A flag indicating the class of substitution tokens to use. If an + * object is passed in the second param, 'types' should contain the + * object's type. For example, 'node', 'comment', or 'user'. 'types' + * may also be an array of types of the form array('node','user'). If no + * type is specified, only 'global' site-wide substitution tokens are + * built. + * + * @return + * The array of usable tokens and their descriptions, organized by + * token type. + */ +function token_get_list($types = 'all') { + token_include(); + $return = array(); + settype($types, 'array'); + foreach (module_implements('token_list') as $module) { + foreach ($types as $type) { + $module_token_list = module_invoke($module, 'token_list', $type); + if (isset($module_token_list) && is_array($module_token_list)) { + foreach ($module_token_list as $category => $tokens) { + foreach ($tokens as $token => $title) { + // Automatically append a raw token warning. + if (substr($token, -4) === '-raw' && strpos($title, t('raw user input')) === FALSE && strpos($title, t('UNIX timestamp format')) === FALSE) { + $title .= ' ' . t('Warning: Token value contains raw user input.') . ''; + } + $return[$category][$token] = $title; + } + } + } + } + } + // Sort the tokens by name. + foreach (array_keys($return) as $category) { + ksort($return[$category]); + } + return $return; +} + +/** + * A helper function to prepare raw tokens for replacement. + * + * @param $tokens + * The array of tokens names with no delimiting characters. + * @param $leading + * String to prepend to the token. Default is TOKEN_PREFIX. + * @param $trailing + * String to append to the token. Default is TOKEN_SUFFIX. + * + * @return + * An array of the formatted tokens. + */ +function token_prepare_tokens($tokens = array(), $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX) { + foreach ($tokens as $key => $value) { + $tokens[$key] = $leading . $value . $trailing; + } + return $tokens; +} + +/** + * A helper function to return an object's ID for use in static caching. + */ +function _token_get_id($type = 'global', $object = NULL) { + if (!isset($object)) { + return "default"; + } + switch ($type) { + case 'node': + return isset($object->vid) ? $object->vid : (isset($object->nid) ? $object->nid : 0); + case 'comment': + return isset($object->cid) ? $object->cid : 0; + case 'user': + return isset($object->uid) ? $object->uid : 0; + case 'taxonomy': + return isset($object->tid) ? $object->tid : 0; + default: + return crc32(serialize($object)); + } +} + +/** + * Build a list of common date tokens for use in hook_token_list(). + * + * @param $description + */ +function token_get_date_token_info($description, $token_prefix = '') { + $time = time(); + $tokens[$token_prefix . 'small'] = t("!description date in 'small' format. (%date)", array('!description' => $description, '%date' => format_date($time, 'small'))); + $tokens[$token_prefix . 'yyyy'] = t("!description year (four digit)", array('!description' => $description)); + $tokens[$token_prefix . 'yy'] = t("!description year (two digit)", array('!description' => $description)); + $tokens[$token_prefix . 'month'] = t("!description month (full word)", array('!description' => $description)); + $tokens[$token_prefix . 'mon'] = t("!description month (abbreviated)", array('!description' => $description)); + $tokens[$token_prefix . 'mm'] = t("!description month (two digits with leading zeros)", array('!description' => $description)); + $tokens[$token_prefix . 'm'] = t("!description month (one or two digits without leading zeros)", array('!description' => $description)); + $tokens[$token_prefix . 'ww'] = t("!description week (two digits with leading zeros)", array('!description' => $description)); + if (version_compare(PHP_VERSION, '5.1.0', '>=')) { + $tokens[$token_prefix . 'date'] = t("!description date (numeric representation of the day of the week)", array('!description' => $description)); + } + $tokens[$token_prefix . 'day'] = t("!description day (full word)", array('!description' => $description)); + $tokens[$token_prefix . 'ddd'] = t("!description day (abbreviation)", array('!description' => $description)); + $tokens[$token_prefix . 'dd'] = t("!description day (two digits with leading zeros)", array('!description' => $description)); + $tokens[$token_prefix . 'd'] = t("!description day (one or two digits without leading zeros)", array('!description' => $description)); + $tokens[$token_prefix . 'raw'] = t("!description in UNIX timestamp format (%date)", array('!description' => $description, '%date' => $time)); + $tokens[$token_prefix . 'since'] = t("!description in 'time-since' format. (%date)", array('!description' => $description, '%date' => format_interval($time - 360, 2))); + return $tokens; +} + +/** + * Build a list of common date tokens for use in hook_token_values(). + */ +function token_get_date_token_values($timestamp = NULL, $token_prefix = '', $langcode = NULL) { + static $formats; + + if (!isset($formats)) { + $formats = array(); + $formats['small'] = variable_get('date_format_short', 'm/d/Y - H:i'); + $formats['yyyy'] = 'Y'; + $formats['yy'] = 'y'; + $formats['month'] = 'F'; + $formats['mon'] = 'M'; + $formats['mm'] = 'm'; + $formats['m'] = 'n'; + $formats['ww'] = 'W'; + if (version_compare(PHP_VERSION, '5.1.0', '>=')) { + $formats['date'] = 'N'; + } + $formats['day'] = 'l'; + $formats['ddd'] = 'D'; + $formats['dd'] = 'd'; + $formats['d'] = 'j'; + } + + $time = time(); + if (!isset($timestamp)) { + $timestamp = $time; + } + + $tokens = array(); + foreach ($formats as $token => $format) { + $tokens[$token_prefix . $token] = token_format_date($timestamp, 'custom', $format, NULL, $langcode); + } + $tokens[$token_prefix . 'raw'] = $timestamp; + $tokens[$token_prefix . 'since'] = format_interval($time - $timestamp, 2, $langcode); + + return $tokens; +} + +/** + * A copy of format_date() that supports the 'N' date format character. + * + * @see format_date() + */ +function token_format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) { + global $user; + static $timezones = array(); + + // Statically cache each user's timezone so it doesn't need to be re-fetched + // ever call. + if (!isset($timezones[$user->uid])) { + if (!empty($user->uid) && variable_get('configurable_timezones', 1) && strlen($user->timezone)) { + $timezones[$user->uid] = $user->timezone; + } + else { + $timezones[$user->uid] = variable_get('date_default_timezone', 0); + } + } + + $timestamp += $timezones[$user->uid]; + + switch ($type) { + case 'custom': + // No change to format. + break; + case 'small': + $format = variable_get('date_format_short', 'm/d/Y - H:i'); + break; + case 'large': + $format = variable_get('date_format_long', 'l, F j, Y - H:i'); + break; + case 'medium': + default: + $format = variable_get('date_format_medium', 'D, m/d/Y - H:i'); + } + + $max = strlen($format); + $date = ''; + for ($i = 0; $i < $max; $i++) { + $c = $format[$i]; + if (strpos('AaDlM', $c) !== FALSE) { + $date .= t(gmdate($c, $timestamp), array(), $langcode); + } + elseif ($c == 'F') { + // Special treatment for long month names: May is both an abbreviation + // and a full month name in English, but other languages have + // different abbreviations. + $date .= trim(t('!long-month-name ' . gmdate($c, $timestamp), array('!long-month-name' => ''), $langcode)); + } + elseif (strpos('BdgGhHiIjLmnNsStTUwWYyz', $c) !== FALSE) { + // This condition was modified to allow the 'N' date format character. + $date .= gmdate($c, $timestamp); + } + elseif ($c == 'r') { + $date .= token_format_date($timestamp - $timezone, 'custom', 'D, d M Y H:i:s O', $timezone, $langcode); + } + elseif ($c == 'O') { + $date .= sprintf('%s%02d%02d', ($timezone < 0 ? '-' : '+'), abs($timezone / 3600), abs($timezone % 3600) / 60); + } + elseif ($c == 'Z') { + $date .= $timezone; + } + elseif ($c == '\\') { + $date .= $format[++$i]; + } + else { + $date .= $c; + } + } + + return $date; +} + +/** + * Validate an tokens in raw text based on possible contexts. + * + * @param $value + * A string with the raw text containing the raw tokens, or an array of + * tokens from token_scan(). + * @param $valid_types + * An array of token types to validage against. + * @param $leading + * Character(s) to prepend to the token key before searching for + * matches. Defaults to TOKEN_PREFIX. + * @param $trailing + * Character(s) to append to the token key before searching for + * matches. Defaults to TOKEN_SUFFIX. + * + * @return + * An array with the invalid tokens in their original raw forms. + */ +function token_get_invalid_tokens_by_context($value, $valid_types = array(), $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX) { + if (in_array('all', $valid_types)) { + $valid_types = array('all'); + } + else { + // Add the token types that are always valid in global context. + $valid_types[] = 'global'; + } + + $invalid_tokens = array(); + $valid_tokens = array(); + $value_tokens = is_string($value) ? token_scan($value, $leading, $trailing) : $value; + + foreach (token_get_list($valid_types) as $category => $tokens) { + $valid_tokens += $tokens; + } + + foreach ($value_tokens as $token) { + if (isset($valid_tokens[$token])) { + continue; + } + elseif (preg_match('/^(.*[_-])([^-_])+$/', $token, $matches)) { + // Allow tokens that do not have a direct match to tokens listed in + // hook_token_info() to be matched against a 'wildcard' token name. + if (isset($valid_tokens[$matches[1] . '?'])) { + // [token-name-?] wildcards. + continue; + } + elseif (isset($valid_tokens[$matches[1] . '????'])) { + // [token-name-????] wildcards. + continue; + } + elseif (is_numeric($matches[2]) && isset($valid_tokens[$matches[1] . 'N'])) { + // [token-name-N] wildcards if N is a numeric value. + continue; + } + } + $invalid_tokens[] = $token; + } + + array_unique($invalid_tokens); + $invalid_tokens = token_prepare_tokens($invalid_tokens, $leading, $trailing); + return $invalid_tokens; +} + +/** + * Build a list of all token-like patterns that appear in the text. + * + * @param $text + * The text to be scanned for possible tokens. + * @param $leading + * Character(s) to prepend to the token key before searching for + * matches. Defaults to TOKEN_PREFIX. + * @param $trailing + * Character(s) to append to the token key before searching for + * matches. Defaults to TOKEN_SUFFIX. + * + * @return + * An array of discovered tokens. + */ +function token_scan($text, $leading = TOKEN_PREFIX, $trailing = TOKEN_SUFFIX) { + $leadingregex = preg_quote($leading, '/'); + $trailingregex = preg_quote($trailing, '/'); + + $regex = '/' . $leadingregex; + $regex .= '([^\s'; + if (drupal_strlen($leading) == 1) { + // Only add the leading string as a non-match if it is a single character. + $regex .= $leadingregex; + } + if (drupal_strlen($trailing) == 1) { + // Only add the trailing string as a non-match if it is a single character. + $regex .= $trailingregex; + } + $regex .= ']+)' . $trailingregex . '/x'; + + preg_match_all($regex, $text, $matches); + return $matches[1]; +} + +/** + * Validate a form element that should have tokens in it. + * + * Form elements that want to add this validation should have the #token_types + * parameter defined. + * + * For example: + * @code + * $form['my_node_text_element'] = array( + * '#type' => 'textfield', + * '#title' => t('Some text to token-ize that has a node context.'), + * '#default_value' => 'The title of this node is [title].', + * '#element_validate' => array('token_element_validate'), + * '#token_types' => array('node'), + * '#min_tokens' => 1, + * '#max_tokens' => 10, + * ); + * @endcode + */ +function token_element_validate(&$element, &$form_state) { + $value = isset($element['#value']) ? $element['#value'] : $element['#default_value']; + + if (!drupal_strlen($value)) { + // Empty value needs no further validation since the element should depend + // on using the '#required' FAPI property. + return $element; + } + + $tokens = token_scan($value); + $title = empty($element['#title']) ? $element['#parents'][0] : $element['#title']; + + // Validate if an element must have a minimum number of tokens. + if (isset($element['#min_tokens']) && count($tokens) < $element['#min_tokens']) { + // @todo Change this error message to include the minimum number. + $error = format_plural($element['#min_tokens'], 'The %element-title cannot contain fewer than one token.', 'The %element-title must contain at least @count tokens.', array('%element-title' => $title)); + form_error($element, $error); + } + + // Validate if an element must have a maximum number of tokens. + if (isset($element['#max_tokens']) && count($tokens) > $element['#max_tokens']) { + // @todo Change this error message to include the maximum number. + $error = format_plural($element['#max_tokens'], 'The %element-title must contain as most one token.', 'The %element-title must contain at most @count tokens.', array('%element-title' => $title)); + form_error($element, $error); + } + + // Check if the field defines specific token types. + if (!empty($element['#token_types'])) { + $invalid_tokens = token_get_invalid_tokens_by_context($tokens, $element['#token_types']); + if ($invalid_tokens) { + form_error($element, t('The %element-title is using the following invalid tokens: @invalid-tokens.', array('%element-title' => $title, '@invalid-tokens' => implode(', ', $invalid_tokens)))); + } + } + + return $element; +} + +/** + * Deprecated. Use token_element_validate() instead. + */ +function token_element_validate_token_context(&$element, &$form_state) { + return token_element_validate($element, $form_state); +} + +/** + * Find tokens that have been declared twice by different modules. + */ +function token_find_duplicate_tokens() { + token_include(); + $all_tokens = array(); + + foreach (module_implements('token_list') as $module) { + $module_token_list = module_invoke($module, 'token_list', 'all'); + if (!isset($module_token_list) || !is_array($module_token_list)) { + // Skip modules that do not return an array as that is a valid return + // value. + continue; + } + if (in_array($module, _token_core_supported_modules())) { + $module = 'token'; + } + foreach ($module_token_list as $type => $tokens) { + foreach (array_keys($tokens) as $token) { + $all_tokens[$type . ':' . $token][] = $module; + } + } + } + + foreach ($all_tokens as $token => $modules) { + if (count($modules) < 2) { + unset($all_tokens[$token]); + } + } + + return $all_tokens; +} + +/** + * Get a translated menu link by its mlid, without access checking. + * + * This function is a copy of menu_link_load() but with its own cache and a + * simpler query to load the link. This also skips normal menu link access + * checking by using _token_menu_link_translate(). + * + * @param $mlid + * The mlid of the menu item. + * + * @return + * A menu link translated for rendering. + * + * @see menu_link_load() + * @see _token_menu_link_translate() + */ +function token_menu_link_load($mlid) { + static $cache = array(); + + if (!is_numeric($mlid)) { + return FALSE; + } + + if (!isset($cache[$mlid])) { + $item = db_fetch_array(db_query("SELECT * FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid)); + if (!empty($item)) { + _token_menu_link_translate($item); + } + $cache[$mlid] = $item; + } + + return $cache[$mlid]; +} + +/** + * Get a translated book menu link by its mlid, without access checking. + * + * This function is a copy of book_link_load() but with its own cache and a + * simpler query to load the link. This also skips normal menu link access + * checking by using _token_menu_link_translate(). + * + * @param $mlid + * The mlid of the book menu item. + * + * @return + * A book menu link translated for rendering. + * + * @see book_link_load() + * @see _token_menu_link_translate() + */ +function token_book_link_load($mlid) { + static $cache = array(); + + if (!is_numeric($mlid)) { + return FALSE; + } + + if (!isset($cache[$mlid])) { + $item = db_fetch_array(db_query("SELECT * FROM {menu_links} ml INNER JOIN {book} b ON b.mlid = ml.mlid LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid)); + if (!empty($item)) { + _token_menu_link_translate($item); + } + $cache[$mlid] = $item; + } + + return $cache[$mlid]; +} + +function _token_menu_link_translate(&$item) { + $map = array(); + + if (!is_array($item['options'])) { + $item['options'] = unserialize($item['options']); + } + + if ($item['external']) { + $item['access'] = 1; + $item['href'] = $item['link_path']; + $item['title'] = $item['link_title']; + $item['localized_options'] = $item['options']; + } + else { + $map = explode('/', $item['link_path']); + _menu_link_map_translate($map, $item['to_arg_functions']); + $item['href'] = implode('/', $map); + + // Note - skip callbacks without real values for their arguments. + if (strpos($item['href'], '%') !== FALSE) { + $item['access'] = FALSE; + return FALSE; + } + + $item['access'] = TRUE; + _menu_item_localize($item, $map, TRUE); + } + + // Allow other customizations - e.g. adding a page-specific query string to the + // options array. For performance reasons we only invoke this hook if the link + // has the 'alter' flag set in the options array. + if (!empty($item['options']['alter'])) { + drupal_alter('translated_menu_link', $item, $map); + } + + return $map; +} + +/** + * Find all ancestors of a given menu link ID. + * + * @param $mlid + * A menu link ID. + * + * @return + * An array of menu links from token_menu_link_load() with the root link + * first, and the menu link with ID $mlid last. + */ +function token_menu_link_get_parents_all($mlid) { + $parents = array(); + + while (!empty($mlid)) { + $link = token_menu_link_load($mlid); + array_unshift($parents, $link); + $mlid = $link['plid']; + } + + return $parents; +} + +/** + * Deprecated. Use the raw return value of token_menu_link_get_parents_all() instead. + */ +function _menu_titles($menu_link, $nid) { + $titles = array(); + $parents = token_menu_link_get_parents_all($menu_link['mlid']); + foreach ($parents as $mlid => $parent) { + $titles[] = $parent['title']; + } + return $titles; +} diff --git a/sites/all/modules/token/token.pages.inc b/sites/all/modules/token/token.pages.inc new file mode 100644 index 0000000..5ff2131 --- /dev/null +++ b/sites/all/modules/token/token.pages.inc @@ -0,0 +1,158 @@ + $category) { + $rows[] = array(array('data' => t('@type tokens', array('@type' => drupal_ucfirst($key))), 'class' => 'region', 'colspan' => 2)); + foreach ($category as $token => $description) { + $row = array(); + $row[] = $prefix . $token . $suffix; + $row[] = $description; + $rows[] = $row; + } + } + + $output = theme('table', $headers, $rows, array('class' => 'description')); + return $output; +} + +/** + * Provide a 'tree' display of nested tokens. + * + * @ingroup themeable + */ +function theme_token_tree($token_types = array(), $global_types = TRUE, $click_insert = TRUE) { + if ($token_types == 'all' || !is_array($token_types) || in_array('all', $token_types)) { + $token_types = array('all'); + } + elseif ($global_types) { + $token_types[] = 'global'; + } + else { + $global_key = array_search('global', $token_types); + if ($global_key !== FALSE) { + unset($token_types[$global_key]); + } + } + + // Check for token type validity and sort. + $token_types = array_unique($token_types); + $info = token_get_list($token_types); + //$token_types = array_intersect($token_types, array_keys($info)); + $token_types = array_keys($info); + sort($token_types); + + $header = array( + t('Token'), + t('Description'), + ); + $rows = array(); + + foreach ($token_types as $type) { + $parent = NULL; + + if (count($token_types) > 1) { + $rows[] = _token_token_tree_format_row($type, array(), TRUE); + $parent = $type; + } + + foreach ($info[$type] as $token => $description) { + $rows[] = _token_token_tree_format_row("[$token]", array('description' => $description, 'parent' => $parent)); + } + } + + if (count($rows)) { + drupal_add_js(drupal_get_path('module', 'token') . '/jquery.treeTable.js'); + drupal_add_css(drupal_get_path('module', 'token') . '/jquery.treeTable.css'); + drupal_add_js(drupal_get_path('module', 'token') . '/token.js'); + drupal_add_css(drupal_get_path('module', 'token') . '/token.css'); + } + else { + $rows[] = array(array( + 'data' => t('No tokens available.'), + 'colspan' => 2, + )); + } + + $table_options = array( + 'attributes' => array('class' => 'token-tree'), + 'caption' => '', + ); + if ($click_insert) { + $table_options['caption'] = t('Click a token to insert it into the field you\'ve last clicked.'); + $table_options['attributes']['class'] .= ' token-click-insert'; + } + return theme('table', $header, $rows, $table_options['attributes'], $table_options['caption']); +} + +/** + * Build a row in the token tree. + */ +function _token_token_tree_format_row($token, $token_info = array(), $is_group = FALSE) { + $row = array( + 'id' => _token_clean_css_identifier($token), + 'class' => array(), + 'data' => array( + 'token' => '', + 'description' => !empty($token_info['description']) ? $token_info['description'] : '', + ), + ); + + if ($is_group) { + // This is a token type/group. + $row['data']['token'] = drupal_ucfirst($token); + $row['class'][] = 'token-group'; + $row['id'] .= '-group'; + } + else { + // This is a token. + $row['data']['token'] = array( + 'data' => $token, + 'class' => 'token-key', + ); + if (!empty($token_info['parent'])) { + $row['class'][] = 'child-of-' . _token_clean_css_identifier($token_info['parent']) . '-group'; + } + } + + $row['class'] = implode(' ', $row['class']); + + return $row; +} + +function _token_clean_css_identifier($id) { + return 'token-' . rtrim(str_replace(array('][', '_', ' ', ':'), '-', trim($id, '[]')), '-'); +} + +/** + * Menu callback; prints the available tokens and values for an object. + */ +function token_devel_token_object($entity, $object) { + $tokens = token_get_values($entity, $object); + $tokens = array_combine($tokens->tokens, $tokens->values); + return kdevel_print_object($tokens); +} diff --git a/sites/all/modules/token/token.rules.inc b/sites/all/modules/token/token.rules.inc new file mode 100644 index 0000000..81afe62 --- /dev/null +++ b/sites/all/modules/token/token.rules.inc @@ -0,0 +1,120 @@ + array( + 'label' => t('Token replacement patterns'), + 'weight' => -5, + ), + ); +} + +/** + * Prepares the evalution. + * + * @param $string + * The string to evaluate later. + * @param $variables + * An array of available variables. + * @return + * Arbitrary data, which is passed to the evaluator on evaluation. + * If NULL is returned the input evaluator will be skipped later. + */ +function token_rules_input_evaluator_prepare($string, $variables) { + $used_vars = array(); + foreach ($variables as $name => $info) { + if (strpos($string, TOKEN_PREFIX. $name .':') !== FALSE) { + $used_vars[] = $name; + } + } + // Using ':global' instead of 'global' to avoid potential namespace conflicts + // See http://drupal.org/node/932460#comment-3884866 + $used_vars[] = ':global'; + return $used_vars ? $used_vars : NULL; +} + +/** + * Apply the input evaluator. + * + * @param $text + * The string for which tokens should be replaced. + * @param $used_vars + * The used variables as returned from preparation. + * @param $state + * The current evaluation state of rules. + */ +function token_rules_input_evaluator_apply($text, $used_vars, &$state) { + static $token_cache = array(); + + if ($used_vars) { + $vars = rules_get_variables(drupal_map_assoc(array_diff($used_vars, array(':global'))), $state); + if ($vars === FALSE) { + //there not all needed variables available! + return FALSE; + } + $vars[':global'] = ':global'; + + foreach ($used_vars as $name) { + $type = ($name == ':global') ? 'global' : _token_rules_map_type($state['variables'][$name]->info['type']); + if ($type) { + $token_id = _token_get_id($type, $vars[$name]); + if (isset($token_cache[$token_id]) && $token_cache[$token_id] != $name) { + // this is the same variable in another state + // so we need to flush the token cache to get the fresh values + token_get_values('reset'); + } + + $text = token_replace($text, $type, $vars[$name], TOKEN_PREFIX. $name .':', TOKEN_SUFFIX); + + // remember that this variable has been used and got cached + $token_cache[$token_id] = $name; + } + } + } + + return $text; +} + +/** + * Map rules types to corresponding token types + */ +function _token_rules_map_type($type) { + if (($data_type = rules_get_data_types($type)) && isset($data_type['token type'])) { + return $data_type['token type']; + } + return $type; +} + +/** + * Some token replacement help for the condition/action edit form. + */ +function token_rules_input_evaluator_help($variables) { + + $variables[':global'] = array('type' => 'global', 'label' => t('global token'),); + + foreach ($variables as $name => $info) { + $type = _token_rules_map_type($info['type']); + if ($type) { + $form[$name] = array( + '#type' => 'fieldset', + '#title' => t('Replacement patterns for @name', array('@name' => $info['label'])), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form[$name]['content'] = array( + '#value' => theme('token_help', $type, TOKEN_PREFIX. $name . ':', TOKEN_SUFFIX), + ); + } + } + return $form; +} diff --git a/sites/all/modules/token/token.test b/sites/all/modules/token/token.test new file mode 100644 index 0000000..1a08eaf --- /dev/null +++ b/sites/all/modules/token/token.test @@ -0,0 +1,690 @@ +assertTokens($type, $object, array($token => $expected), $options); + } + + function assertTokens($type, $object, array $tokens, array $options = array()) { + $values = token_get_values($type, $object, FALSE, $options); + $values = array_combine($values->tokens, $values->values); + foreach ($tokens as $token => $expected) { + if (!isset($expected)) { + $this->assertTrue(!isset($values[$token]), t("Token value for [@token] was not generated.", array('@token' => $token))); + } + elseif (!isset($values[$token])) { + $this->fail(t("Token value for [@token] was not generated.", array('@token' => $token))); + } + else { + $this->assertIdentical($values[$token], $expected, t("Token value for [@token] was '@actual', expected value '@expected'.", array('@token' => $token, '@actual' => $values[$token], '@expected' => $expected))); + } + } + } + + /** + * Make a page request and test for token generation. + */ + function assertPageTokens($url, array $tokens, array $data = array('global' => NULL), array $options = array()) { + if (empty($tokens)) { + return TRUE; + } + + $token_page_tokens = array( + 'tokens' => $tokens, + 'data' => $data, + 'options' => $options, + ); + variable_set('token_page_tokens', $token_page_tokens); + + $options += array('url_options' => array()); + $this->drupalGet($url, $options['url_options']); + $this->refreshVariables(); + $result = variable_get('token_page_tokens', array()); + + if (!isset($result['values']) || !is_array($result['values'])) { + return $this->fail('Failed to generate tokens.'); + } + + foreach ($tokens as $token => $expected) { + if (!isset($expected)) { + $this->assertTrue(!isset($result['values'][$token]) || $result['values'][$token] === $token, t("Token value for @token was not generated.", array('@token' => $token))); + } + elseif (!isset($result['values'][$token])) { + $this->fail(t('Failed to generate token @token.', array('@token' => $token))); + } + else { + $this->assertIdentical($result['values'][$token], (string) $expected, t("Token value for @token was '@actual', expected value '@expected'.", array('@token' => $token, '@actual' => $result['values'][$token], '@expected' => $expected))); + } + } + } +} + +class TokenUnitTestCase extends TokenTestHelper { + public static function getInfo() { + return array( + 'name' => 'Token unit tests', + 'description' => 'Test basic, low-level token functions.', + 'group' => 'Token', + ); + } + + /** + * Test token_get_invalid_tokens() and token_get_invalid_tokens_by_context(). + */ + public function testGetInvalidTokens() { + $tests = array(); + $tests[] = array( + 'valid tokens' => array( + '[title-raw]', + '[yyyy]', + '[mod-yyyy]', + '[site-name]', + '[site-slogan]', + '[user-id]', + ), + 'invalid tokens' => array( + '[title-invalid]', + '[invalid]', + '[mod-invalid]', + '[invalid-title]', + '[site-invalid]', + '[uid]', + '[comment-cid]', + ), + 'types' => array('node'), + ); + $tests[] = array( + 'valid tokens' => array( + '[title-raw]', + '[yyyy]', + '[mod-yyyy]', + '[site-name]', + '[site-slogan]', + '[user-id]', + '[uid]', + '[comment-cid]', + ), + 'invalid tokens' => array( + '[title-invalid]', + '[invalid]', + '[mod-invalid]', + '[invalid-title]', + '[site-invalid]', + ), + 'types' => array('all'), + ); + $tests[] = array( + 'valid tokens' => array( + '[alpha]', + '[beta-1]', + '[beta-2]', + '[gamma_A]', + '[delta-extra]', + '[epsilon-zeta-A]', + ), + 'invalid tokens' => array( + '[alpha-plus]', + '[beta]', + '[beta-]', + '[beta_]', + '[beta_1]', + '[beta-A]', + '[gamma]', + '[gamma_]', + '[gamma-A]', + '[delta]', + '[epsilon-zeta-]', + ), + 'types' => array('all'), + ); + + foreach ($tests as $test) { + $tokens = array_merge($test['valid tokens'], $test['invalid tokens']); + shuffle($tokens); + + $invalid_tokens = token_get_invalid_tokens_by_context(implode(' ', $tokens), $test['types']); + + sort($invalid_tokens); + sort($test['invalid tokens']); + $this->assertEqual($invalid_tokens, $test['invalid tokens'], 'Invalid tokens detected properly: ' . implode(', ', $invalid_tokens)); + } + } + + /** + * Test the $options['clear'] parameter for token_replace(). + */ + public function testClearOption() { + $tests[] = array( + 'input' => 'Foo [site-name][invalid-token] bar [another-invalid-token] [invalid-token]', + 'output' => 'Foo Drupal bar ', + 'options' => array('clear' => TRUE), + ); + $tests[] = array( + 'input' => 'Foo [site-name][invalid-token] bar [another-invalid-token] [invalid-token]', + 'output' => 'Foo Drupal[invalid-token] bar [another-invalid-token] [invalid-token]', + 'options' => array(), + ); + + foreach ($tests as $test) { + $output = token_replace($test['input'], 'global', NULL, TOKEN_PREFIX, TOKEN_SUFFIX, $test['options']); + $this->assertIdentical($output, $test['output']); + } + } + + /** + * Test whether token-replacement works in various contexts. + * + * @see http://drupal.org/node/733192 + */ + function testSystemTokenRecognition() { + global $language; + + // Generate prefixes and suffixes for the token context. + $tests = array( + array('prefix' => 'this is the ', 'suffix' => ' site'), + array('prefix' => 'this is the', 'suffix' => 'site'), + array('prefix' => '[', 'suffix' => ']'), + array('prefix' => '', 'suffix' => ']]]'), + array('prefix' => '[[[', 'suffix' => ''), + array('prefix' => ':[:', 'suffix' => '--]'), + array('prefix' => '-[-', 'suffix' => ':]:'), + array('prefix' => '[:', 'suffix' => ']'), + array('prefix' => '[site:', 'suffix' => ':name]'), + array('prefix' => '[site:', 'suffix' => ']'), + ); + + // Check if the token is recognized in each of the contexts. + foreach ($tests as $test) { + $input = $test['prefix'] . '[site-name]' . $test['suffix']; + $expected = $test['prefix'] . 'Drupal' . $test['suffix']; + $output = token_replace($input); + $this->assertEqual($output, $expected); + } + } + + /** + * Test token caching. + */ + function testTokenCaching() { + // Run global tokens once so that the cache is primed. + $tokens = array( + 'option-foo' => '', + ); + $this->assertTokens('global', NULL, $tokens); + + // Run global tokens again with different options. This should return a + // different value for the [option-foo] token. + $tokens = array( + 'option-foo' => 'bar', + ); + $this->assertTokens('global', NULL, $tokens, array('foo' => 'bar')); + } + + /** + * Test the token_scan() function. + */ + function testTokenScan() { + $tests = array( + array('text' => 'Test [foo] [[bar]] test.', 'tokens' => array('foo', 'bar')), + array('text' => 'Test [foo] [] test.', 'tokens' => array('foo')), + array('text' => 'Test [foo][] test.', 'tokens' => array('foo')), + array('text' => 'Test [foo][bar] test.', 'tokens' => array('foo', 'bar')), + // Test the e-mail token syntax. + array('text' => 'Test %foo %%bar test.', 'tokens' => array('foo', 'bar'), 'leading' => '%', 'trailing' => ''), + array('text' => 'Test %foo % test.', 'tokens' => array('foo'), 'leading' => '%', 'trailing' => ''), + array('text' => 'Test %foo% test.', 'tokens' => array('foo'), 'leading' => '%', 'trailing' => ''), + array('text' => 'Test %foo%%bar test.', 'tokens' => array('foo', 'bar'), 'leading' => '%', 'trailing' => ''), + // Test the rules token syntax. + array('text' => 'Test [global:foo] [global:bar] test.', 'tokens' => array('foo', 'bar'), 'leading' => '[global:'), + array('text' => 'Test [node:foo] [node:] test.', 'tokens' => array('foo'), 'leading' => '[node:'), + array('text' => 'Test [node:foo][node:] test.', 'tokens' => array('foo'), 'leading' => '[node:'), + array('text' => 'Test [node:foo][node:bar] test.', 'tokens' => array('foo', 'bar'), 'leading' => '[node:'), + ); + foreach ($tests as $test) { + $test += array('leading' => TOKEN_PREFIX, 'trailing' => TOKEN_SUFFIX); + $this->assertEqual(token_scan($test['text'], $test['leading'], $test['trailing']), $test['tokens']); + } + } +} + +class TokenNodeTestCase extends TokenTestHelper { + public static function getInfo() { + return array( + 'name' => 'Node token tests', + 'description' => 'Test the node tokens.', + 'group' => 'Token', + ); + } + + function testNodeTokens() { + $time = time(); + $created = gmmktime(0, 0, 0, 11, 19, 1978); + $changed = gmmktime(0, 0, 0, 7, 4, 1984); + $node = $this->drupalCreateNode(array( + 'type' => 'page', + 'language' => 'und', + 'created' => $created, + 'log' => '' . $this->randomName() . '', + )); + $node->changed = $changed; + path_set_alias('node/' . $node->nid, 'content/first-node'); + + $tokens = array( + 'nid' => $node->nid, + 'type' => 'page', + 'type-name' => 'Page', + 'language' => 'und', + 'node-path' => 'content/first-node', + 'node-url' => url('node/' . $node->nid, array('absolute' => TRUE)), + 'small' => '11/19/1978 - 00:00', + 'yyyy' => '1978', + 'yy' => '78', + 'month' => 'November', + 'mon' => 'Nov', + 'mm' => '11', + 'm' => '11', + 'ww' => '46', + 'date' => '7', + 'day' => 'Sunday', + 'ddd' => 'Sun', + 'dd' => '19', + 'd' => '19', + 'raw' => 280281600, + 'since' => format_interval($time - 280281600), + 'mod-small' => '07/04/1984 - 00:00', + 'mod-yyyy' => '1984', + 'mod-yy' => '84', + 'mod-month' => 'July', + 'mod-mon' => 'Jul', + 'mod-mm' => '07', + 'mod-m' => '7', + 'mod-ww' => '27', + 'mod-date' => '3', + 'mod-day' => 'Wednesday', + 'mod-ddd' => 'Wed', + 'mod-dd' => '04', + 'mod-d' => '4', + 'mod-raw' => 457747200, + 'mod-since' => format_interval($time - 457747200), + 'log' => filter_xss($node->log), + 'log-raw' => $node->log, + ); + $this->assertTokens('node', $node, $tokens); + + // Check that a new revision of a node returns different tokens. + $node->revision = TRUE; + $node->title = 'New revision'; + node_save($node); + $this->assertTokens('node', $node, array('title' => 'New revision')); + } +} + +class TokenCommentTestCase extends TokenTestHelper { + protected $node; + + public static function getInfo() { + return array( + 'name' => 'Comment token tests', + 'description' => 'Test the comment tokens.', + 'group' => 'Token', + ); + } + + function setUp(array $modules = array()) { + $modules[] = 'comment'; + parent::setUp($modules); + $this->node = $this->drupalCreateNode(array('comment' => 2)); + } + + function loadComment($cid) { + return db_fetch_object(db_query('SELECT c.cid, c.pid, c.nid, c.subject, c.comment, c.format, c.timestamp, c.name, c.mail, c.homepage, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data, c.status FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d', $cid)); + } + + function createComment(array $comment) { + $comment += array( + 'cid' => 0, + 'nid' => $this->node->nid, + 'pid' => 0, + 'uid' => 0, + 'subject' => $this->randomName(), + 'comment' => $this->randomName(64), + 'format' => 1, + 'timestamp' => gmmktime(0, 0, 0, 7, 4, 1984), + 'status' => COMMENT_PUBLISHED, + ); + + $cid = comment_save($comment); + return $this->loadComment($cid); + } + + function testCommentTokens() { + $time = time(); + $comment = $this->createComment(array( + 'timestamp' => gmmktime(0, 0, 0, 7, 4, 1984), + )); + + $tokens = array( + 'comment-cid' => $comment->cid, + 'comment-nid' => $this->node->nid, + 'comment-yyyy' => '1984', + 'comment-yy' => '84', + 'comment-month' => 'July', + 'comment-mon' => 'Jul', + 'comment-mm' => '07', + 'comment-m' => '7', + 'comment-ww' => '27', + 'comment-date' => '3', + 'comment-day' => 'Wednesday', + 'comment-ddd' => 'Wed', + 'comment-dd' => '04', + 'comment-d' => '4', + 'comment-raw' => '457747200', + 'comment-since' => format_interval($time - 457747200), + 'comment-node-title' => check_plain($this->node->title), + 'comment-node-title-raw' => $this->node->title, + ); + $this->assertTokens('comment', $comment, $tokens); + + } +} + +class TokenTaxonomyTestCase extends TokenTestHelper { + protected $vocabulary; + + public static function getInfo() { + return array( + 'name' => 'Taxonomy and vocabulary token tests', + 'description' => 'Test the taxonomy tokens.', + 'group' => 'Token', + ); + } + + function setUp(array $modules = array()) { + $modules[] = 'taxonomy'; + parent::setUp($modules); + // Reset the static taxonomy.module caches. + taxonomy_vocabulary_load(0, TRUE); + taxonomy_get_term(0, TRUE); + } + + function addVocabulary(array $vocabulary = array()) { + $vocabulary += array( + 'name' => drupal_strtolower($this->randomName(5)), + 'nodes' => array('story' => 'story'), + ); + taxonomy_save_vocabulary($vocabulary); + return (object) $vocabulary; + } + + function addTerm(stdClass $vocabulary, array $term = array()) { + $term += array( + 'name' => drupal_strtolower($this->randomName(5)), + 'vid' => $vocabulary->vid, + ); + taxonomy_save_term($term); + return (object) $term; + } + + function testTaxonomyTokens() { + $vocabulary = $this->addVocabulary(array( + 'name' => 'Vocab Name', + 'description' => 'Vocab Description', + )); + $term = $this->addTerm($vocabulary, array( + 'name' => 'Term Name', + 'description' => 'Term Description', + )); + + $tokens = array( + 'tid' => $term->tid, + 'cat' => check_plain($term->name), + 'cat-raw' => $term->name, + 'cat-description' => 'Term Description', + 'vid' => $vocabulary->vid, + 'vocab' => check_plain($vocabulary->name), + 'vocab-raw' => $vocabulary->name, + 'vocab-description' => 'Vocab Description', + 'vocab-description-raw' => $vocabulary->description, + ); + $this->assertTokens('taxonomy', $term, $tokens); + + $tokens = array( + 'vocabulary-vid' => $vocabulary->vid, + 'vocabulary-name' => check_plain($vocabulary->name), + 'vocabulary-name-raw' => $vocabulary->name, + 'vocabulary-description' => 'Vocab Description', + 'vocabulary-description-raw' => $vocabulary->description, + ); + $this->assertTokens('vocabulary', $vocabulary, $tokens); + } +} + +class TokenMenuTestCase extends TokenTestHelper { + public static function getInfo() { + return array( + 'name' => 'Menu token tests', + 'description' => 'Test the menu tokens.', + 'group' => 'Token', + ); + } + + function setUp(array $modules = array()) { + $modules[] = 'menu'; + parent::setUp($modules); + } + + function testMenuTokens() { + $root_link = array( + 'link_path' => 'root', + 'link_title' => 'Root link', + 'menu_name' => 'primary-links', + ); + menu_link_save($root_link); + + // Add another link with the root link as the parent + $parent_link = array( + 'link_path' => 'root/parent', + 'link_title' => 'Parent link', + 'menu_name' => 'primary-links', + 'plid' => $root_link['mlid'], + ); + menu_link_save($parent_link); + + $node_link = array( + 'enabled' => TRUE, + 'link_title' => 'Node link', + 'plid' => $parent_link['mlid'], + 'customized' => 0, + ); + $node = $this->drupalCreateNode(array('menu' => $node_link)); + + // Test [node:menu] tokens. + $tokens = array( + 'menu' => 'Primary links', + 'menu-raw' => 'Primary links', + 'menupath' => 'Root link/Parent link/Node link', + 'menupath-raw' => 'Root link/Parent link/Node link', + 'menu-link-title' => 'Node link', + 'menu-link-title-raw' => 'Node link', + 'menu-link-mlid' => $node->menu['mlid'], + 'menu-link-plid' => $node->menu['plid'], + 'menu-link-plid' => $parent_link['mlid'], + 'menu-link-parent-path' => 'root/parent', + 'menu-link-parent-path-raw' => 'root/parent', + ); + $this->assertTokens('node', $node, $tokens); + + // Reload the node which will not have $node->menu defined and re-test. + $loaded_node = node_load($node->nid); + // We have to reset the token static cache because tokens are cached by + // node ID only and not if the node object has changed. + $this->assertTokens('node', $loaded_node, $tokens, array('reset' => TRUE)); + + // Regression test for http://drupal.org/node/1317926 to ensure the + // original node object is not changed when calling menu_node_prepare(). + $this->assertTrue(!isset($loaded_node->menu), t('The $node->menu property was not modified during token replacement.'), 'Regression'); + + } +} + +/* + * Unit tests for the book tokens provided by Pathauto. + */ +class TokenBookTestCase extends TokenTestHelper { + public static function getInfo() { + return array( + 'name' => 'Book tokens', + 'description' => 'Tests the book tokens.', + 'group' => 'Token', + ); + } + + function setUp(array $modules = array()) { + $modules[] = 'book'; + $modules[] = 'menu'; + parent::setUp($modules); + + variable_set('book_allowed_types', array('book', 'page')); + } + + function testBookTokens() { + // Add a non-book node. + $non_book_node = $this->drupalCreateNode(array('type' => 'book')); + $tokens = array( + 'book' => '', + 'book-raw' => '', + 'book_id' => '', + 'bookpath' => '', + 'bookpath-raw' => '', + ); + $this->assertTokens('node', $non_book_node, $tokens); + + // Add a root book page. + $parent_node = $this->drupalCreateNode(array( + 'type' => 'book', + 'title' => 'Root', + 'book' => array('bid' => 'new') + _book_link_defaults('new'), + )); + $tokens = array( + 'book' => 'Root', + 'book-raw' => 'Root', + 'book_id' => $parent_node->book['bid'], + 'bookpath' => '', + 'bookpath-raw' => '', + // Check that even those book menu links have been created for this node, + // that the menu links still return nothing. + 'menu' => '', + 'menupath' => '', + 'menu-link-title' => '', + 'menu-link-title-raw' => '', + 'menu-link-mlid' => '', + 'menu-link-plid' => '', + 'menu-link-parent-path' => '', + ); + $this->assertTokens('node', $parent_node, $tokens); + + // Add a first child page. + $child_node1 = $this->drupalCreateNode(array( + 'type' => 'book', + 'title' => 'Sub page1', + 'book' => array( + 'bid' => $parent_node->book['bid'], + 'plid' => $parent_node->book['mlid'], + ) + _book_link_defaults('new'), + )); + $tokens = array( + 'book' => 'Root', + 'book-raw' => 'Root', + 'book_id' => $parent_node->book['bid'], + 'bookpath' => 'Root', + 'bookpath-raw' => 'Root', + ); + $this->assertTokens('node', $child_node1, $tokens); + + // Add a second child page. + $child_node2 = $this->drupalCreateNode(array( + 'type' => 'book', + 'title' => 'Sub page2', + 'book' => array( + 'bid' => $parent_node->book['bid'], + 'plid' => $parent_node->book['mlid'], + ) + _book_link_defaults('new'), + )); + $tokens = array( + 'book' => 'Root', + 'book-raw' => 'Root', + 'book_id' => $parent_node->book['bid'], + 'bookpath' => 'Root', + 'bookpath-raw' => 'Root', + ); + $this->assertTokens('node', $child_node2, $tokens); + + // Add a child page on an existing child page. + $sub_child_node1 = $this->drupalCreateNode(array( + 'type' => 'page', + 'title' => 'Sub-sub Page1', + 'book' => array( + 'bid' => $parent_node->book['bid'], + 'plid' => $child_node1->book['mlid'], + ) + _book_link_defaults('new'), + )); + $tokens = array( + 'book' => 'Root', + 'book-raw' => 'Root', + 'book_id' => $parent_node->book['bid'], + 'bookpath' => 'Root/Sub page1', + 'bookpath-raw' => 'Root/Sub page1', + ); + $this->assertTokens('node', $sub_child_node1, $tokens); + } +} + +/** + * Test the current page tokens. + */ +class TokenCurrentPageTestCase extends TokenTestHelper { + public static function getInfo() { + return array( + 'name' => 'Current page token tests', + 'description' => 'Test the current page tokens.', + 'group' => 'Token', + ); + } + + function testCurrentPageTokens() { + $tokens = array( + '[current-page-title]' => '', + '[current-page-path]' => 'node', + '[current-page-url]' => url('node', array('absolute' => TRUE)), + '[current-page-number]' => 1, + ); + $this->assertPageTokens('', $tokens); + + $node = $this->drupalCreateNode(array('title' => 'Node title', 'path' => 'node-alias')); + $tokens = array( + '[current-page-title]' => 'Node title', + '[current-page-path]' => 'node-alias', + '[current-page-url]' => url("node/{$node->nid}", array('absolute' => TRUE)), + '[current-page-number]' => 1, + ); + $this->assertPageTokens("node/{$node->nid}", $tokens); + } +} diff --git a/sites/all/modules/token/tokenSTARTER.info b/sites/all/modules/token/tokenSTARTER.info new file mode 100644 index 0000000..3f7e70b --- /dev/null +++ b/sites/all/modules/token/tokenSTARTER.info @@ -0,0 +1,11 @@ +name = TokenSTARTER +description = Provides additional tokens and a base on which to build your own tokens. +dependencies[] = token +core = 6.x + +; Information added by drupal.org packaging script on 2012-09-12 +version = "6.x-1.19" +core = "6.x" +project = "token" +datestamp = "1347470077" + diff --git a/sites/all/modules/token/tokenSTARTER.module b/sites/all/modules/token/tokenSTARTER.module new file mode 100644 index 0000000..8c9dfa2 --- /dev/null +++ b/sites/all/modules/token/tokenSTARTER.module @@ -0,0 +1,68 @@ + array( + 'type' => 'system', + 'description' => t('Display a tokenized message to the user'), + 'configurable' => TRUE, + 'hooks' => array( + 'nodeapi' => array('view', 'insert', 'update', 'delete'), + 'comment' => array('view', 'insert', 'update', 'delete'), + 'user' => array('view', 'insert', 'update', 'delete', 'login'), + 'taxonomy' => array('insert', 'update', 'delete'), + ), + ), + 'token_actions_send_email_action' => array( + 'description' => t('Send tokenized e-mail'), + 'type' => 'system', + 'configurable' => TRUE, + 'hooks' => array( + 'nodeapi' => array('view', 'insert', 'update', 'delete'), + 'comment' => array('view', 'insert', 'update', 'delete'), + 'user' => array('view', 'insert', 'update', 'delete', 'login'), + 'taxonomy' => array('insert', 'update', 'delete'), + ) + ), + 'token_actions_goto_action' => array( + 'description' => t('Redirect to a tokenized URL'), + 'type' => 'system', + 'configurable' => TRUE, + 'hooks' => array( + 'nodeapi' => array('view', 'insert', 'update', 'delete'), + 'comment' => array('view', 'insert', 'update', 'delete'), + 'user' => array('view', 'insert', 'update', 'delete', 'login'), + ) + ) + ); +} + +/** + * Implements hook_mail(). + */ +function token_actions_mail($key, &$message, $params) { + $message['subject'] = $params['subject']; + $message['body'][] = $params['body']; +} + +/** + * Action callback to send a tokenized e-mail. + * + * @see token_actions_send_email_action_form() + * @see token_actions_send_email_action_submit() + */ +function token_actions_send_email_action($object, $context) { + token_normalize_context($context); + $params['from'] = variable_get('site_mail', ini_get('sendmail_from')); + $recipient = token_replace_multiple($context['recipient'], $context); + $params['subject'] = str_replace(array("\r", "\n"), '', token_replace_multiple($context['subject'], $context)); + $params['body'] = token_replace_multiple($context['message'], $context); + + if (drupal_mail('token_actions', 'action_send_email', $recipient, language_default(), $params)) { + watchdog('action', 'Sent email to %recipient', array('%recipient' => $recipient)); + } + else { + watchdog('error', 'Unable to send email to %recipient', array('%recipient' => $recipient)); + } +} + +/** + * Form builder; Prepare a form for a tokenized e-mail action. + * + * @see token_actions_send_email_action() + * @see token_actions_send_email_action_validate() + * @see token_actions_send_email_action_submit() + */ +function token_actions_send_email_action_form($context) { + // Set default values for form. + $context += array( + 'recipient' => '', + 'subject' => '', + 'message' => '', + ); + + $form['recipient'] = array( + '#type' => 'textfield', + '#title' => t('Recipient'), + '#default_value' => $context['recipient'], + '#required' => TRUE, + '#size' => '20', + '#maxlength' => '254', + '#description' => t('The email address to which the message should be sent.'), + '#element_validate' => array('token_element_validate'), + '#token_types' => array('all'), + ); + $form['subject'] = array( + '#type' => 'textfield', + '#title' => t('Subject'), + '#default_value' => $context['subject'], + '#size' => '20', + '#maxlength' => '254', + '#description' => t('The subject of the message.'), + '#element_validate' => array('token_element_validate'), + '#token_types' => array('all'), + ); + $form['message'] = array( + '#type' => 'textarea', + '#title' => t('Message'), + '#default_value' => $context['message'], + '#required' => TRUE, + '#cols' => '80', + '#rows' => '20', + '#description' => t('The message that should be sent.'), + '#element_validate' => array('token_element_validate'), + '#token_types' => array('all'), + ); + + $form['help'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Placeholder tokens'), + '#description' => t("The following placeholder tokens can be used in to generate the URL path. Some tokens may not be available, depending on the context in which the action is triggered."), + ); + $form['help']['tokens'] = array( + '#value' => theme('token_tree', 'all'), + ); + + return $form; +} + +/** + * Validate token_actions_send_email_action form submissions. + * + * @see token_actions_send_email_action() + * @see token_actions_send_email_action_form() + * @see token_actions_send_email_action_submit() + */ +function token_actions_send_email_action_validate($form, $form_state) { + $form_values = $form_state['values']; + if (!valid_email_address($form_values['recipient']) && strpos($form_values['recipient'], 'mail') === FALSE) { + // We want the literal %mail placeholder to be emphasized in the error message. + form_set_error('recipient', t('Enter a valid email address or use a token e-mail address such as %mail.', array('%mail' => '[mail]'))); + } +} + +/** + * Process token_actions_send_email_action form submissions. + * + * @see token_actions_send_email_action() + * @see token_actions_send_email_action_form() + * @see token_actions_send_email_action_validate() + */ +function token_actions_send_email_action_submit($form, $form_state) { + return array( + 'recipient' => $form_state['values']['recipient'], + 'subject' => $form_state['values']['subject'], + 'message' => $form_state['values']['message'], + ); +} + +/** + * Action callback to send a message to the current user's screen. + * + * @see token_actions_message_action_form() + * @see token_actions_message_action_submit() + */ +function token_actions_message_action(&$object, $context = array()) { + token_normalize_context($context); + $context['message'] = token_replace_multiple($context['message'], $context); + drupal_set_message($context['message']); +} + +/** + * Form builder; Prepare a form for a tokenized message action. + * + * @see token_actions_message_action() + * @see token_actions_message_action_submit() + */ +function token_actions_message_action_form($context) { + $context += array('message' => ''); + + $form['message'] = array( + '#type' => 'textarea', + '#title' => t('Message'), + '#default_value' => $context['message'], + '#required' => TRUE, + '#rows' => '8', + '#description' => t('The message to be displayed to the current user.'), + '#element_validate' => array('token_element_validate'), + '#token_types' => array('all'), + ); + + $form['help'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Placeholder tokens'), + '#description' => t("The following placeholder tokens can be used in the custom message text. Some tokens may not be available, depending on the context in which the action is triggered."), + ); + $form['help']['tokens'] = array( + '#value' => theme('token_tree', 'all'), + ); + + return $form; +} + +/** + * Process token_actions_message_action form submissions. + * + * @see token_actions_message_action() + * @see token_actions_message_action_form() + */ +function token_actions_message_action_submit($form, $form_state) { + return array('message' => $form_state['values']['message']); +} + +/** + * Action callback to redirect the user to a tokenized URL. + * + * @see token_actions_goto_action_form() + * @see token_actions_goto_action_submit() + */ +function token_actions_goto_action($object, $context) { + token_normalize_context($context); + drupal_goto(token_replace_multiple($context['url'], $context)); +} + +/** + * Form builder; Prepare a form for a tokenized redirect action. + * + * @see token_actions_goto_action() + * @see token_actions_goto_action_submit() + */ +function token_actions_goto_action_form($context) { + $context += array('url' => ''); + + $form['url'] = array( + '#type' => 'textfield', + '#title' => t('URL'), + '#description' => t('The URL to which the user should be redirected. This can be an internal URL like node/1234 or an external URL like http://drupal.org.'), + '#default_value' => $context['url'], + '#required' => TRUE, + '#element_validate' => array('token_element_validate'), + '#token_types' => array('all'), + ); + + $form['help'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Placeholder tokens'), + '#description' => t("The following placeholder tokens can be used in the URL path. Some tokens may not be available, depending on the context in which the action is triggered."), + ); + $form['help']['tokens'] = array( + '#value' => theme('token_tree', 'all'), + ); + + return $form; +} + +/** + * Process token_actions_goto_action form submissions. + * + * @see token_actions_goto_action() + * @see token_actions_goto_action_form() + */ +function token_actions_goto_action_submit($form, $form_state) { + return array('url' => $form_state['values']['url']); +} + +/** + * Normalize an action context for use with token_replace_multiple(). + */ +function token_normalize_context(&$context) { + $context['global'] = NULL; + + if (empty($context['user']) && !empty($context['node'])) { + $context['user'] = user_load(array('uid' => $context['node']->uid)); + } + if (empty($context['node']) && !empty($context['comment']) && !empty($context['comment']->nid)) { + $context['node'] = node_load($context['comment']->nid); + } + if (empty($context['user']) && !empty($context['account'])) { + $context['user'] = $context['account']; + } +} diff --git a/sites/all/modules/token/token_actions.test b/sites/all/modules/token/token_actions.test new file mode 100644 index 0000000..9c77063 --- /dev/null +++ b/sites/all/modules/token/token_actions.test @@ -0,0 +1,79 @@ + t('Token action tests'), + 'description' => t('Test some of the token actions and tokens.'), + 'group' => t('Token'), + ); + } + + function setUp() { + parent::setUp('token', 'token_actions', 'trigger'); + $user = $this->drupalCreateUser(array('administer actions', 'administer site configuration', 'administer users')); + $this->drupalLogin($user); + } + + /** + * Test user actions and triggers. + */ + function testUserActions() { + $insert_action = $this->createAction('token_actions_message_action', array( + 'message' => 'Yay [site-name] has a new user [user] with an ID of [uid] and e-mail address of [mail]!', + )); + $this->assignTriggerAction('user', 'insert', $insert_action); + + // Create a user to trigger the action. + $edit = array(); + $edit['name'] = $this->randomName(); + $edit['mail'] = $edit['name'] .'@example.com'; + $edit['pass[pass1]'] = $this->randomName(); + $edit['pass[pass2]'] = $edit['pass[pass1]']; + + $this->drupalPost('admin/user/user/create', $edit, t('Create new account')); + $account = user_load(array('name' => $edit['name'])); + $this->assertText("Yay Drupal has a new user {$account->name} with an ID of {$account->uid} and e-mail address of {$account->mail}!", 'Tokenized message displays'); + } + + /** + * Create an action. + * + * @param $action + * The machine name of the action. + * @param $edit + * An optional array to pass onto drupalPost() for configuring the action. + * + * @return + * The created action object. + */ + function createAction($action, $edit = array()) { + $edit += array( + 'actions_description' => $this->randomName(), + ); + $this->drupalPost('admin/settings/actions/configure/'. md5($action), $edit, t('Save')); + $this->assertText('The action has been successfully saved.'); + return db_fetch_object(db_query("SELECT * FROM {actions} WHERE type = 'system' AND callback = '%s' AND description = '%s'", $action, $edit['actions_description'])); + } + + /** + * Assign an action to a trigger. + * + * @param $type + * The trigger type. + * @param $trigger + * The trigger. + * @param $action + * The action object. + */ + function assignTriggerAction($type, $trigger, $action) { + $edit['aid'] = md5($action->aid); + $this->drupalPost("admin/build/trigger/{$type}", $edit, 'Assign', array(), array(), "trigger-{$type}-{$trigger}-assign-form"); + return $this->assertLinkByHref("admin/build/trigger/unassign/{$type}/{$trigger}/{$edit['aid']}", 0, t('Action assigned to @type @trigger trigger.', array('@type' => $type, '@trigger' => $trigger))); + } +} diff --git a/sites/all/modules/token/token_comment.inc b/sites/all/modules/token/token_comment.inc new file mode 100644 index 0000000..c7fdc3b --- /dev/null +++ b/sites/all/modules/token/token_comment.inc @@ -0,0 +1,91 @@ +cid; + $values['comment-nid'] = $comment->nid; + $values['comment-title'] = check_plain($comment->subject); + $values['comment-body'] = check_markup($comment->comment, $comment->format, FALSE); + $values['comment-author-name'] = check_plain($comment->name); + $values['comment-author-uid'] = $comment->uid; + $values['comment-author-homepage'] = check_url($comment->homepage); + + // Raw counterparts of user supplied data. + $values['comment-title-raw'] = $comment->subject; + $values['comment-body-raw'] = $comment->comment; + $values['comment-author-name-raw'] = $comment->name; + + if (!empty($comment->mail)) { + $account_mail = $comment->mail; + } + elseif (!empty($comment->uid)) { + $account_mail = db_result(db_query("SELECT mail FROM {users} WHERE uid = %d", $comment->uid)); + } + else { + $account_mail = ''; + } + $values['comment-author-mail'] = check_plain($account_mail); + $values['comment-author-mail-raw'] = $account_mail; + + // Included in case a consuming module wants to format the body + $values['comment-body-format'] = $comment->format; + + $values += token_get_date_token_values($comment->timestamp, 'comment-'); + + $values['comment-node-title-raw'] = db_result(db_query("SELECT title FROM {node} WHERE nid = %d", $comment->nid)); + $values['comment-node-title'] = check_plain($values['comment-node-title-raw']); + } + + return $values; +} diff --git a/sites/all/modules/token/token_node.inc b/sites/all/modules/token/token_node.inc new file mode 100644 index 0000000..e892fa8 --- /dev/null +++ b/sites/all/modules/token/token_node.inc @@ -0,0 +1,328 @@ +uid)); + + // Adjust for the anonymous user name. + if (!$node->uid && !$account->name) { + $account->name = variable_get('anonymous', t('Anonymous')); + } + + $values['nid'] = $node->nid; + $values['type'] = $node->type; + $values['type-name'] = node_get_types('name', $node->type); + $values['language'] = check_plain($node->language); + $values['title'] = check_plain($node->title); + $values['title-raw'] = $node->title; + $values['node-path-raw'] = drupal_get_path_alias('node/'. $node->nid); + $values['node-path'] = check_plain($values['node-path-raw']); + $values['node-url'] = url('node/' . $node->nid, array('absolute' => TRUE)); + $values['author-uid'] = $node->uid; + $values['author-name'] = check_plain($account->name); + $values['author-name-raw'] = $account->name; + $values['author-mail'] = check_plain($account->mail); + $values['author-mail-raw'] = $account->mail; + + $values['log-raw'] = isset($node->log) ? $node->log : ''; + $values['log'] = filter_xss($values['log-raw']); + + if (module_exists('comment')) { + $values['node_comment_count'] = isset($node->comment_count) ? $node->comment_count : 0; + $values['unread_comment_count'] = comment_num_new($node->nid); + } + else { + $values['node_comment_count'] = 0; + $values['unread_comment_count'] = 0; + } + + if (isset($node->created)) { + $values += token_get_date_token_values($node->created, ''); + } + + if (isset($node->changed)) { + $values += token_get_date_token_values($node->changed, 'mod-'); + } + + // And now taxonomy, which is a bit more work. This code is adapted from + // pathauto's handling code; it's intended for compatibility with it. + if (module_exists('taxonomy') && !empty($node->taxonomy) && is_array($node->taxonomy)) { + foreach ($node->taxonomy as $term) { + $original_term = $term; + if ((object)$term) { + // With free-tagging it's somewhat hard to get the tid, vid, name values + // Rather than duplicating taxonomy.module code here you should make sure your calling module + // has a weight of at least 1 which will run after taxonomy has saved the data which allows us to + // pull it out of the db here. + if (!isset($term->name) || !isset($term->tid)) { + $vid = db_result(db_query_range("SELECT t.vid FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.nid = %d ORDER BY v.weight, t.weight, t.name", $object->nid, 0, 1)); + if (!$vid) { + continue; + } + $term = db_fetch_object(db_query_range("SELECT t.tid, t.name FROM {term_data} t INNER JOIN {term_node} r ON r.tid = t.tid WHERE t.vid = %d AND r.vid = %d ORDER BY t.weight", $vid, $object->vid, 0, 1)); + $term->vid = $vid; + } + + // Ok, if we still don't have a term name maybe this is a pre-taxonomy submit node + // So if it's a number we can get data from it + if (!isset($term->name) && is_array($original_term)) { + $tid = array_shift($original_term); + if (is_numeric($tid)) { + $term = taxonomy_get_term($tid); + } + } + $values['term'] = check_plain($term->name); + $values['term-raw'] = $term->name; + $values['term-id'] = $term->tid; + $vid = $term->vid; + + if (!empty($vid)) { + $vocabulary = taxonomy_vocabulary_load($vid); + $values['vocab'] = check_plain($vocabulary->name); + $values['vocab-raw'] = $vocabulary->name; + $values['vocab-id'] = $vocabulary->vid; + } + + // The 'catpath' (and 'cat') tokens have been removed, as they caused quite a bit of confusion, + // and the catpath was a relatively expensive query when the taxonomy tree was deep. + // + // It existed only to provide forward-compatability with pathauto module, and + // for most uses of token.module, it was a relatively useless token -- it exposed + // a list of term names formatted as a URL/path string. Once pathauto supports + // tokens, *it* should handle this catpath alias as it's the primary consumer. + break; + } + } + } + // It's possible to leave that block and still not have good data. + // So, we test for these and if not set, set them. + if (!isset($values['term'])) { + $values['term'] = ''; + $values['term-raw'] = ''; + $values['term-id'] = ''; + $values['vocab'] = ''; + $values['vocab-raw'] = ''; + $values['vocab-id'] = ''; + } + } + + return $values; +} + +/** + * Implements hook_token_list() on behalf of menu.module. + */ +function menu_token_list($type = 'all') { + $tokens = array(); + + if ($type == 'node' || $type == 'all') { + $tokens['node']['menu'] = t("The name of the menu the node belongs to."); + $tokens['node']['menu-raw'] = t("The name of the menu the node belongs to."); + $tokens['node']['menupath'] = t("The menu path (as reflected in the breadcrumb), not including Home or [menu]. Separated by /."); + $tokens['node']['menupath-raw'] = t("The unfiltered menu path (as reflected in the breadcrumb), not including Home or [menu]. Separated by /."); + $tokens['node']['menu-link-title'] = t("The text used in the menu as link text for this item."); + $tokens['node']['menu-link-title-raw'] = t("The unfiltered text used in the menu as link text for this item."); + $tokens['node']['menu-link-mlid'] = t("The unique ID of the node's menu link."); + $tokens['node']['menu-link-plid'] = t("The unique ID of the node's menu link parent."); + $tokens['node']['menu-link-parent-path'] = t('The URL alias of the parent menu link of the node.'); + $tokens['node']['menu-link-parent-path-raw'] = t('The URL alias of the parent menu link of the node.'); + } + + return $tokens; +} + +/** + * Implements hook_token_values() on behalf of menu.module. + */ +function menu_token_values($type, $object = NULL, $options = array()) { + static $menus; + + $values = array(); + + // Statically cache menu_get_menus() since this causes a database query + // every time it is called, and is not likely to change in the same page + // request. + if (!isset($menus)) { + $menus = menu_get_menus(); + } + + if ($type == 'node' && !empty($object)) { + $node = $object; + + // Initialize tokens to empty strings. + $values['menu'] = ''; + $values['menu-raw'] = ''; + $values['menupath'] = ''; + $values['menupath-raw'] = ''; + $values['menu-link-title'] = ''; + $values['menu-link-title-raw'] = ''; + $values['menu-link-mlid'] = ''; + $values['menu-link-plid'] = ''; + $values['menu-link-parent-path'] = ''; + $values['menu-link-parent-path-raw'] = ''; + + if (!isset($node->menu)) { + // We need to clone the node as menu_nodeapi($node, 'prepare') may cause data loss. + // @see http://drupal.org/node/1317926 + $node = drupal_clone($node); + // Nodes do not have their menu links loaded via menu_nodeapi($node, 'load'). + menu_nodeapi($node, 'prepare'); + } + + // Now get the menu related information. + if (!empty($node->menu['mlid'])) { + $link = token_menu_link_load($node->menu['mlid']); + + if (isset($menus[$link['menu_name']])) { + $values['menu-raw'] = $menus[$link['menu_name']]; + $values['menu'] = check_plain($values['menu-raw']); + } + + $parents = token_menu_link_get_parents_all($link['mlid']); + $trail_raw = array(); + foreach ($parents as $parent) { + $trail_raw[] = $parent['title']; + } + $trail = array_map('check_plain', $trail_raw); + + $values['menupath'] = !empty($options['pathauto']) ? $trail : implode('/', $trail); + $values['menupath-raw'] = !empty($options['pathauto']) ? $trail_raw : implode('/', $trail_raw); + $values['menu-link-title'] = check_plain($link['title']); + $values['menu-link-title-raw'] = $link['title']; + $values['menu-link-mlid'] = $link['mlid']; + + if (!empty($link['plid']) && $parent = token_menu_link_load($link['plid'])) { + $values['menu-link-plid'] = $parent['mlid']; + $values['menu-link-parent-path-raw'] = drupal_get_path_alias($parent['href']); + $values['menu-link-parent-path'] = check_plain($values['menu-link-parent-path-raw']); + } + } + } + + return $values; +} + +/** + * Implements hook_token_list() on behalf of book.module. + */ +function book_token_list($type) { + $tokens = array(); + + if ($type == 'node' || $type == 'all') { + $tokens['book']['book'] = t("The title of the node's book parent."); + $tokens['book']['book-raw'] = t("The title of the node's book parent."); + $tokens['book']['book_id'] = t("The id of the node's book parent."); + $tokens['book']['bookpath'] = t("The titles of all parents in the node's book hierarchy."); + $tokens['book']['bookpath-raw'] = t("The titles of all parents in the node's book hierarchy."); + } + + return $tokens; +} + +/** + * Implements hook_token_values() on behalf of book.module. + */ +function book_token_values($type, $object = NULL, $options = array()) { + $values = array(); + + if ($type == 'node' && !empty($object)) { + $node = $object; + + // Initialize tokens to empty strings. + $values['book'] = ''; + $values['book-raw'] = ''; + $values['book_id'] = ''; + $values['bookpath'] = ''; + $values['bookpath-raw'] = ''; + + if (!empty($node->book['mlid'])) { + // Exclude the current node's title from the book path trail (start with + // the book link's plid rather than mlid). + $parents = token_menu_link_get_parents_all($node->book['plid']); + $trail_raw = array(); + foreach ($parents as $parent) { + $trail_raw[] = $parent['title']; + } + $trail = array_map('check_plain', $trail_raw); + + // Load the root book page. + $root = token_menu_link_load($node->book['p1']); + + $values['book'] = check_plain($root['title']); + $values['book-raw'] = $root['title']; + $values['book_id'] = $node->book['bid']; + $values['bookpath'] = !empty($options['pathauto']) ? $trail : implode('/', $trail); + $values['bookpath-raw'] = !empty($options['pathauto']) ? $trail_raw : implode('/', $trail_raw); + } + } + + return $values; +} diff --git a/sites/all/modules/token/token_taxonomy.inc b/sites/all/modules/token/token_taxonomy.inc new file mode 100644 index 0000000..a9c7050 --- /dev/null +++ b/sites/all/modules/token/token_taxonomy.inc @@ -0,0 +1,81 @@ +vid); + + $values['tid'] = $term->tid; + $values['cat'] = check_plain($term->name); + $values['cat-raw'] = $term->name; + $values['cat-description'] = filter_xss($term->description); + $values['vid'] = $term->vid; + $values['vocab'] = check_plain($vocabulary->name); + $values['vocab-raw'] = $vocabulary->name; + $values['vocab-description'] = filter_xss($vocabulary->description); + $values['vocab-description-raw'] = $vocabulary->description; + } + + // Vocabulary tokens. + if ($type == 'vocabulary' && !empty($object)) { + $vocabulary = $object; + + $values['vocabulary-vid'] = $vocabulary->vid; + $values['vocabulary-name'] = check_plain($vocabulary->name); + $values['vocabulary-name-raw'] = $vocabulary->name; + $values['vocabulary-description'] = filter_xss($vocabulary->description); + $values['vocabulary-description-raw'] = $vocabulary->description; + } + + return $values; +} diff --git a/sites/all/modules/token/token_user.inc b/sites/all/modules/token/token_user.inc new file mode 100644 index 0000000..9aba610 --- /dev/null +++ b/sites/all/modules/token/token_user.inc @@ -0,0 +1,78 @@ + $GLOBALS['user']->uid)); + + // Adjust for the anonymous user name. + if (!$account->uid && empty($account->name)) { + $account_name = variable_get('anonymous', 'Anonymous'); + } + else { + $account_name = $account->name; + } + + $values['user'] = check_plain($account_name); + $values['user-raw'] = $account_name; + $values['uid'] = $account->uid; + $values['mail'] = $account->uid ? $account->mail : ''; + + if ($account->uid) { + $values += token_get_date_token_values($account->created, 'user-created-'); + $values += token_get_date_token_values($account->access, 'user-last-login-'); + $values['reg-date'] = $values['user-created-small']; + $values['reg-since'] = $values['user-created-since']; + $values['log-date'] = $values['user-last-login-small']; + $values['log-since'] = $values['user-last-login-since']; + $values['date-in-tz'] = $account->uid ? format_date(time(), 'small', '', $account->timezone) : ''; + } + + $values['account-url'] = $account->uid ? url("user/$account->uid", array('absolute' => TRUE)) : ''; + $values['account-edit-url'] = $account->uid ? url("user/$account->uid/edit", array('absolute' => TRUE)) : ''; + $values['account-edit'] = $values['account-edit-url']; + } + + return $values; +}