diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f092eec --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +*.patch +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..20d1595 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,95 @@ +language: php + +php: + - 5.4 + - 5.5 + +mysql: + database: drupal + username: root + encoding: utf8 + +addons: + hosts: + - travis.dev + +env: + global: + - MODULE_NAME='flag' + - MODULE_TEST_GROUP='flag' + - DRUPAL_REPO='git://drupalcode.org/project/drupal.git' + - DRUPAL_VERSION='8.0.x' + - PHPCS_VERSION='2.0.*@dev' + - CODER_VERSION='dev-8.x-2.x' + +before_install: + # Install Apache and FastCGI extension to connect to PHP-FPM. + - sudo apt-get update > /dev/null + - sudo apt-get install apache2 libapache2-mod-fastcgi > /dev/null + - sudo a2enmod rewrite actions fastcgi alias + - sudo cp -f .travis/vhost.conf /etc/apache2/sites-available/default + - sudo sed -i -e "s,/var/www,`pwd`,g" /etc/apache2/sites-available/default + + # Start PHP-FPM. There is no process manager available to start PHP-FPM on + # Travis CI currently, so we have to locate and enable it manually. + - sudo cp $HOME/.phpenv/versions/`php -r "print PHP_VERSION;"`/etc/php-fpm.conf.default $HOME/.phpenv/versions/`php -r "print PHP_VERSION;"`/etc/php-fpm.conf + - $HOME/.phpenv/versions/`php -r "print PHP_VERSION;"`/sbin/php-fpm + + # Import the PHP configuration. + - phpenv config-add .travis/php.ini + + # Always update Composer to the recent version, otherwise the drush + # installation fails. + - composer selfupdate + +install: + # Add composer's global bin directory to the path. + # @see: https://github.com/drush-ops/drush#install---composer + - export PATH="$HOME/.composer/vendor/bin:$PATH" + + # install drush globally + - composer global require drush/drush:dev-master + - composer global require squizlabs/php_codesniffer:$PHPCS_VERSION + - composer global require drupal/coder:$CODER_VERSION + +before_script: + # Create a Drupal coding standard reference in PHPCS coding standards. + - ln -s ~/.composer/vendor/drupal/coder/coder_sniffer/Drupal ~/.composer/vendor/squizlabs/php_codesniffer/CodeSniffer/Standards + # Remember the current flag test directory for later use in the Drupal + # installation. + - TESTDIR=$(pwd) + # Navigate out of module directory to prevent blown stack by recursive module + # lookup. + - cd .. + + # Create new site, stubbing sendmail path with true to prevent delivery errors + # and manually resolving drush path. + - mysql -e 'create database drupal' + # Download Drupal 8 core. + - git clone --depth 1 --branch $DRUPAL_VERSION $DRUPAL_REPO drupal + - cd drupal + # Install Drupal. + - php -d sendmail_path=`which true` ~/.composer/vendor/bin/drush.php --yes --uri=http://travis.dev site-install --db-url=mysql://root:@localhost/drupal testing + + # Reference and enable flag in build site. + - ln -s $TESTDIR modules/$MODULE_NAME + - drush --yes pm-enable simpletest $MODULE_NAME + + # Start a web server on port 8080, run in the background; wait for + # initialization. This is temporarly disabled since there are no web tests + # yet. + - sudo apachectl restart + - until netstat -an 2>/dev/null | grep '80.*LISTEN'; do true; done + +script: + # Run the Coder sniffer for Flag. + - phpcs --report=full --standard=Drupal ./modules/$MODULE_NAME + # Run the Simpletests for Flag. + - php ./core/scripts/run-tests.sh --php `which php` --concurrency 12 --url http://travis.dev --verbose --color "$MODULE_TEST_GROUP" + # Run the PHPUnit tests. + - ./core/vendor/phpunit/phpunit/phpunit -c ./core/phpunit.xml.dist ./modules/$MODULE_NAME + +after_failure: + - echo "Failures detected. Outputing additional logs:" + - sudo cat /var/log/apache2/error.log + - sudo cat /var/log/mysql/error.log diff --git a/.travis/php.ini b/.travis/php.ini new file mode 100644 index 0000000..3891e56 --- /dev/null +++ b/.travis/php.ini @@ -0,0 +1,16 @@ +; +; INI file used to populate PHP settings to Travis CI. +; +; This file is included by the .travis.yml configuration in the root of the +; Flag repository. Configuration set here will be utilized when the testing +; suite is run by Travis CI. +; + +; Disable mail sending. +sendmail_path = 'true' + +; Set memory limit. +memory_limit = 128M + +; Ensure a default of fixing paths is enabled. +cgi.fix_pathinfo = 1 \ No newline at end of file diff --git a/.travis/vhost.conf b/.travis/vhost.conf new file mode 100644 index 0000000..dddccac --- /dev/null +++ b/.travis/vhost.conf @@ -0,0 +1,41 @@ +# +# Apache VirtualHost configuration used by Travis CI. +# +# This configuration file will be placed into the default Apache configuration +# on test machines, making it so that the localhost URL will be able to serve +# pages generated by PHP-FPM. +# + + ServerAdmin webmaster@localhost + DocumentRoot /var/www + ServerName travis.dev + + + Options FollowSymLinks + AllowOverride None + + + # This directory will be replaced with the appropriate web root by the + # .travis.yml configuration. + + Options Indexes FollowSymLinks MultiViews ExecCGI + AllowOverride All + Order allow,deny + Allow from all + + + # Wire up Apache to use Travis CI's php-fpm. + + AddHandler php5-fcgi .php + Action php5-fcgi /php5-fcgi + Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi + FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -idle-timeout 120 -host 127.0.0.1:9000 -pass-header Authorization + + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel warn + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..28fa40b --- /dev/null +++ b/LICENSE @@ -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. + + Github sandbox for the Drupal 8 version of Flag module + Copyright (C) 2013 socketwench + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..299406a --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +flag-drupal8 [![Build Status](https://travis-ci.org/socketwench/flag-drupal8.svg?branch=master)](https://travis-ci.org/socketwench/flag-drupal8) +============ + +Github sandbox for the Drupal 8 version of Flag module diff --git a/config/schema/flag.schema.yml b/config/schema/flag.schema.yml new file mode 100644 index 0000000..f3dbb6c --- /dev/null +++ b/config/schema/flag.schema.yml @@ -0,0 +1,56 @@ +flag.flag.*: + type: config_entity + label: 'Flag' + mapping: + id: + type: string + label: 'Flag identifier' + uuid: + type: string + label: 'UUID' + label: + type: label + label: 'Label' + types: + type: sequence + sequence: + - type: string + entity_type: + type: string + label: 'Flaggable Entity Type' + enabled: + type: boolean + label: 'Is flag enabled' + is_global: + type: boolean + label: 'Is flag global' + flag_short: + type: string + label: 'Flag Link Text' + flag_long: + type: string + label: 'Flag Description' + flag_message: + type: string + label: 'Flag Message' + unflag_short: + type: string + label: 'Unflag Link Text' + unflag_long: + type: string + label: 'Unflag Description' + unflag_message: + type: string + label: 'Unflag Message' + unflag_denied_text: + type: string + label: 'Unflag Denied Text' + weight: + type: integer + label: 'Weight' + flag_type: + type: string + label: 'ID of the Flag Type plugin' + link_type: + type: string + label: 'ID of the Link Type plugin' diff --git a/flag.api.php b/flag.api.php new file mode 100644 index 0000000..5017c5f --- /dev/null +++ b/flag.api.php @@ -0,0 +1,174 @@ + array( - 'title' => t('Nodes'), - 'description' => t("Nodes are a Drupal site's primary content."), - 'handler' => 'flag_node', - ), - 'comment' => array( - 'title' => t('Comments'), - 'description' => t('Comments are responses to node content.'), - 'handler' => 'flag_comment', - ), - 'user' => array( - 'title' => t('Users'), - 'description' => t('Users who have created accounts on your site.'), - 'handler' => 'flag_user', - ), - ); -} - -/** - * Returns a flag definition. - */ -function flag_fetch_definition($content_type = NULL) { - static $cache; - if (!isset($cache)) { - $cache = module_invoke_all('flag_definitions'); - if (!isset($cache['node'])) { - // We want our API to be available in hook_install, but our module is not - // enabled by then, so let's load our implementation directly: - $cache += flag_flag_definitions(); - } - } - - if (isset($content_type)) { - if (isset($cache[$content_type])) { - return $cache[$content_type]; - } - } - else { - return $cache; - } -} - -/** - * Returns all flag types defined on the system. - */ -function flag_get_types() { - static $types; - if (!isset($types)) { - $types = array_keys(flag_fetch_definition()); - } - return $types; -} - -/** - * Instantiates a new flag handler. A flag handler is more commonly know as "a - * flag". A factory method usually populates this empty flag with settings - * loaded from the database. - */ -function flag_create_handler($content_type) { - $definition = flag_fetch_definition($content_type); - if (isset($definition) && class_exists($definition['handler'])) { - $handler = new $definition['handler']; - } - else { - $handler = new flag_broken; - } - $handler->content_type = $content_type; - $handler->construct(); - return $handler; -} - -/** - * This abstract class represents a flag, or, in Views 2 terminology, "a handler". - * - * This is the base class for all flag implementations. Notable derived - * classes are flag_node and flag_comment. - */ -class flag_flag { - - // The database ID. Null for flags that haven't been saved to the database yet. - var $fid = NULL; - - // The content-type this flag works with. - var $content_type = NULL; - - // The flag's "machine readable" name. - var $name = ''; - - // Various non-serialized properties of the flag, corresponding directly to - // database columns. - var $title = ''; - var $global = FALSE; - // The sub-types, e.g. node types, this flag applies to. - var $types = array(); - - /** - * Creates a flag from a database row. Returns it. - * - * This is static method. - * - * The reason this isn't a non-static instance method --like Views's init()-- - * is because the class to instantiate changes according to the 'content_type' - * database column. This design pattern is known as the "Single Table - * Inheritance". - */ - static function factory_by_row($row) { - $flag = flag_create_handler($row->content_type); - - // Lump all data unto the object... - foreach ($row as $field => $value) { - $flag->$field = $value; - } - // ...but skip the following two. - unset($flag->options, $flag->type); - - // Populate the options with the defaults. - $options = (array) unserialize($row->options); - $options += $flag->options(); - - // Make the unserialized options accessible as normal properties. - foreach ($options as $option => $value) { - $flag->$option = $value; - } - - if (!empty($row->type)) { - // The loop loading from the database should further populate this property. - $flag->types[] = $row->type; - } - - return $flag; - } - - /** - * Create a complete flag (except an FID) from an array definition. - */ - static function factory_by_array($config) { - $flag = flag_create_handler($config['content_type']); - - foreach ($config as $option => $value) { - $flag->$option = $value; - } - - if (isset($config['locked']) && is_array($config['locked'])) { - $flag->locked = drupal_map_assoc($config['locked']); - } - - return $flag; - } - - /** - * Another factory method. Returns a new, "empty" flag; e.g., one suitable for - * the "Add new flag" page. - */ - static function factory_by_content_type($content_type) { - return flag_create_handler($content_type); - } - - /** - * Declares the options this flag supports, and their default values. - * - * Derived classes should want to override this. - */ - function options() { - $options = array( - 'flag_short' => '', - 'flag_long' => '', - 'flag_message' => '', - 'unflag_short' => '', - 'unflag_long' => '', - 'unflag_message' => '', - 'unflag_denied_text' => '', - 'link_type' => 'toggle', - 'roles' => array( - 'flag' => array(DRUPAL_AUTHENTICATED_RID), - 'unflag' => array(DRUPAL_AUTHENTICATED_RID), - ), - ); - - // Merge in options from the current link type. - $link_type = $this->get_link_type(); - $options = array_merge($options, $link_type['options']); - - // Allow other modules to change the flag options. - drupal_alter('flag_options', $options, $this); - return $options; - } - /** - * Provides a form for setting options. - * - * Derived classes should want to override this. - */ - function options_form(&$form) { - } - - /** - * Default constructor. Loads the default options. - */ - function construct() { - $options = $this->options(); - foreach ($options as $option => $value) { - $this->$option = $value; - } - } - - /** - * Update the flag with settings entered in a form. - */ - function form_input($form_values) { - // Load the form fields indiscriminately unto the flag (we don't care about - // stray FormAPI fields because we aren't touching unknown properties anyway. - foreach ($form_values as $field => $value) { - $this->$field = $value; - } - // But checkboxes need some massaging: - $this->roles['flag'] = array_values(array_filter($this->roles['flag'])); - $this->roles['unflag'] = array_values(array_filter($this->roles['unflag'])); - $this->types = array_values(array_filter($this->types)); - // Clear internal titles cache: - $this->get_title(NULL, TRUE); - } - - /** - * Validates this flag's options. - * - * @return - * A list of errors encountered while validating this flag's options. - */ - function validate() { - // TODO: It might be nice if this used automatic method discovery rather - // than hard-coding the list of validate functions. - return array_merge_recursive( - $this->validate_name(), - $this->validate_access() - ); - } - - /** - * Validates that the current flag's name is valid. - */ - function validate_name() { - $errors = array(); - - // Ensure a safe machine name. - if (!preg_match('/^[a-z_][a-z0-9_]*$/', $this->name)) { - $errors['name'][] = array( - 'error' => 'flag_name_characters', - 'message' => t('The flag name may only contain lowercase letters, underscores, and numbers.'), - ); - } - // Ensure the machine name is unique. - $flag = flag_get_flag($this->name); - if (!empty($flag) && (!isset($this->fid) || $flag->fid != $this->fid)) { - $errors['name'][] = array( - 'error' => 'flag_name_unique', - 'message' => t('Flag names must be unique. This flag name is already in use.'), - ); - } - - return $errors; - } - - /** - * Validates that the current flag's access settings are valid. - */ - function validate_access() { - $errors = array(); - - // Require an unflag access denied message a role is not allowed to unflag. - if (empty($this->unflag_denied_text)) { - foreach ($this->roles['flag'] as $key => $rid) { - if ($rid && empty($this->roles['unflag'][$key])) { - $errors['unflag_denied_text'][] = array( - 'error' => 'flag_denied_text_required', - 'message' => t('The "Unflag not allowed text" is required if any user roles are not allowed to unflag.'), - ); - break; - } - } - } - - // Do not allow unflag access without flag access. - foreach ($this->roles['unflag'] as $key => $rid) { - if ($rid && empty($this->roles['flag'][$key])) { - $errors['roles'][] = array( - 'error' => 'flag_roles_unflag', - 'message' => t('Any user role that has the ability to unflag must also have the ability to flag.'), - ); - break; - } - } - - return $errors; - } - - /** - * Fetches, possibly from some cache, a content object this flag works with. - */ - function fetch_content($content_id, $object_to_remember = NULL) { - static $cache = array(); - if (isset($object_to_remember)) { - $cache[$content_id] = $object_to_remember; - } - if (!array_key_exists($content_id, $cache)) { - $content = $this->_load_content($content_id); - $cache[$content_id] = $content ? $content : NULL; - } - return $cache[$content_id]; - } - - /** - * Loads a content object this flag works with. - * Derived classes must implement this. - * - * @abstract - * @private - * @static - */ - function _load_content($content_id) { - return NULL; - } - - /** - * Stores some object in fetch_content()'s cache, so subsequenet calls to - * fetch_content() return it. - * - * This is needed because otherwise fetch_object() loads the object from the - * database (by calling _load_content()), whereas sometimes we want to fetch - * an object that hasn't yet been saved to the database. See flag_nodeapi(). - */ - function remember_content($content_id, $object) { - $this->fetch_content($content_id, $object); - } - - /** - * @defgroup access Access control - * @{ - */ - - /** - * Returns TRUE if the flag applies to the given content. - * Derived classes must implement this. - * - * @abstract - */ - function applies_to_content_object($content) { - return FALSE; - } - - /** - * Returns TRUE if the flag applies to the content with the given ID. - * - * This is a convenience method that simply loads the object and calls - * applies_to_content_object(). If you already have the object, don't call - * this function: call applies_to_content_object() directly. - */ - function applies_to_content_id($content_id) { - return $this->applies_to_content_object($this->fetch_content($content_id)); - } - - /** - * Returns TRUE if user has access to use this flag. - * - * @param $action - * Optional. The action to test, either "flag" or "unflag". If none given, - * "flag" will be tested, which is the minimum permission to use a flag. - * @param $account - * Optional. The user object. If none given, the current user will be used. - * - * @return - * Boolean TRUE if the user is allowed to flag/unflag. FALSE otherwise. - */ - function user_access($action = 'flag', $account = NULL) { - if (!isset($account)) { - $account = $GLOBALS['user']; - } - - // Anonymous user can't use this system unless Session API is installed. - if ($account->uid == 0 && !module_exists('session_api')) { - return FALSE; - } - - $matched_roles = array_intersect($this->roles[$action], array_keys($account->roles)); - return !empty($matched_roles) || $account->uid == 1; - } - - /** - * Returns TRUE if the user can flag, or unflag, the given content. - * - * @param $content_id - * The content ID to flag/unflag. - * @param $account - * The user on whose behalf to test the flagging action. Leave NULL for the - * current user. - * @param $action - * The action to test. Either 'flag' or 'unflag'. Leave NULL to determine - * by flag status. - */ - function access($content_id, $action = NULL, $account = NULL) { - if (!isset($account)) { - $account = $GLOBALS['user']; - } - - if (isset($content_id) && !$this->applies_to_content_id($content_id)) { - // Flag does not apply to this content. - return FALSE; - } - - if (!isset($action)) { - $uid = $account->uid; - $sid = flag_get_sid($uid); - $action = $this->is_flagged($content_id, $uid, $sid) ? 'unflag' : 'flag'; - } - - // Base initial access on the user's basic permission to use this flag. - $access = $this->user_access($action, $account); - - // Allow modules to disallow (or allow) access to flagging. - $access_array = module_invoke_all('flag_access', $this, $content_id, $action, $account); - - foreach ($access_array as $set_access) { - if (isset($set_access)) { - $access = $set_access; - } - } - - return $access; - } - - /** - * Similar to access() but works on multiple IDs at once. It is called in the - * pre_render() stage of the 'Flag links' field within Views to find out where - * that link applies. The reason we do a separate DB query, and not lump this - * test in the Views query, is to make 'many to one' tests possible without - * interfering with the rows, and also to reduce the complexity of the code. - * - * @param $content_ids - * The array of content IDs to check. The keys are the content IDs, the - * values are the actions to test: either 'flag' or 'unflag'. - * @param $account - * Optional. The account for which the actions will be compared against. - * If left empty, the current user will be used. - */ - function access_multiple($content_ids, $account = NULL) { - $account = isset($account) ? $account : $GLOBALS['user']; - $access = array(); - - // First check basic user access for this action. - foreach ($content_ids as $content_id => $action) { - $access[$content_id] = $this->user_access($content_ids[$content_id], $account); - } - - // Merge in module-defined access. - foreach (module_implements('flag_access_multiple') as $module) { - $module_access = module_invoke($module, 'flag_access_multiple', $this, $content_ids, $account); - foreach ($module_access as $content_id => $content_access) { - if (isset($content_access)) { - $access[$content_id] = $content_access; - } - } - } - - return $access; - } - - /** - * @} End of "defgroup access". - */ - - /** - * Given a content object, returns its ID. - * Derived classes must implement this. - * - * @abstract - */ - function get_content_id($content) { - return NULL; - } - - /** - * Returns TRUE if the flag is configured to show the flag-link using hook_link. - * Derived classes are likely to implement this. - */ - function uses_hook_link($teaser) { - return FALSE; - } - - /** - * Returns TRUE if this flag requires anonymous user cookies. - */ - function uses_anonymous_cookies() { - global $user; - return $user->uid == 0 && variable_get('cache', 0); - } - - /** - * Flags, or unflags, an item. - * - * @param $action - * Either 'flag' or 'unflag'. - * @param $content_id - * The ID of the item to flag or unflag. - * @param $account - * The user on whose behalf to flag. Leave empty for the current user. - * @param $skip_permission_check - * Flag the item even if the $account user don't have permission to do so. - * @return - * FALSE if some error occured (e.g., user has no permission, flag isn't - * applicable to the item, etc.), TRUE otherwise. - */ - function flag($action, $content_id, $account = NULL, $skip_permission_check = FALSE) { - if (!isset($account)) { - $account = $GLOBALS['user']; - } - if (!$account) { - return FALSE; - } - if (!$skip_permission_check) { - if (!$this->access($content_id, $action, $account)) { - // User has no permission to flag/unflag this object. - return FALSE; - } - } - else { - // We are skipping permission checks. However, at a minimum we must make - // sure the flag applies to this content type: - if (!$this->applies_to_content_id($content_id)) { - return FALSE; - } - } - - // Clear various caches; We don't want code running after us to report - // wrong counts or false flaggings. - flag_get_counts(NULL, NULL, TRUE); - flag_get_user_flags(NULL, NULL, NULL, NULL, TRUE); - - // Find out which user id to use. - $uid = $this->global ? 0 : $account->uid; - - // Find out which session id to use. - if ($this->global) { - $sid = 0; - } - else { - $sid = flag_get_sid($uid); - // Anonymous users must always have a session id. - if ($sid == 0 && $account->uid == 0) { - return FALSE; - } - } - - // Perform the flagging or unflagging of this flag. - $flagged = $this->_is_flagged($content_id, $uid, $sid); - if ($action == 'unflag') { - if ($this->uses_anonymous_cookies()) { - $this->_unflag_anonymous($content_id); - } - if ($flagged) { - $fcid = $this->_unflag($content_id, $uid, $sid); - module_invoke_all('flag', 'unflag', $this, $content_id, $account, $fcid); - } - } - elseif ($action == 'flag') { - if ($this->uses_anonymous_cookies()) { - $this->_flag_anonymous($content_id); - } - if (!$flagged) { - $fcid = $this->_flag($content_id, $uid, $sid); - module_invoke_all('flag', 'flag', $this, $content_id, $account, $fcid); - } - } - - return TRUE; - } - - /** - * Determines if a certain user has flagged this content. - * - * Thanks to using a cache, inquiring several different flags about the same - * item results in only one SQL query. - * - * @param $uid - * Optional. The user ID whose flags we're checking. If none given, the - * current user will be used. - * - * @return - * TRUE if the content is flagged, FALSE otherwise. - */ - function is_flagged($content_id, $uid = NULL, $sid = NULL) { - return (bool) $this->get_flagging_record($content_id, $uid, $sid); - } - - /** - * Returns the flagging record. - * - * This method returns the "flagging record": the {flag_content} record that - * exists for each flagged item (for a certain user). If the item isn't - * flagged, returns NULL. This method could be useful, for example, when you - * want to find out the 'fcid' or 'timestamp' values. - * - * Thanks to using a cache, inquiring several different flags about the same - * item results in only one SQL query. - * - * Parameters are the same as is_flagged()'s. - */ - function get_flagging_record($content_id, $uid = NULL, $sid = NULL) { - $uid = $this->global ? 0 : (!isset($uid) ? $GLOBALS['user']->uid : $uid); - $sid = $this->global ? 0 : (!isset($sid) ? flag_get_sid($uid) : $sid); - - // flag_get_user_flags() does caching. - $user_flags = flag_get_user_flags($this->content_type, $content_id, $uid, $sid); - return isset($user_flags[$this->name]) ? $user_flags[$this->name] : NULL; - } - - /** - * Determines if a certain user has flagged this content. - * - * You probably shouldn't call this raw private method: call the - * is_flagged() method instead. - * - * This method is similar to is_flagged() except that it does direct SQL and - * doesn't do caching. Use it when you want to not affect the cache, or to - * bypass it. - * - * @return - * If the content is flagged, returns the value of the 'fcid' column. - * Else, returns FALSE. - * - * @private - */ - function _is_flagged($content_id, $uid, $sid) { - return db_select('flag_content', 'fc') - ->fields('fc', array('fcid')) - ->condition('fid', $this->fid) - ->condition('uid', $uid) - ->condition('sid', $sid) - ->condition('content_id', $content_id) - ->execute() - ->fetchField(); - } - - /** - * A low-level method to flag content. - * - * You probably shouldn't call this raw private method: call the flag() - * function instead. - * - * @return - * The 'fcid' column of the new {flag_content} record. - * - * @private - */ - function _flag($content_id, $uid, $sid) { - $fcid = db_insert('flag_content') - ->fields(array( - 'fid' => $this->fid, - 'content_type' => $this->content_type, - 'content_id' => $content_id, - 'uid' => $uid, - 'sid' => $sid, - 'timestamp' => REQUEST_TIME, - )) - ->execute(); - $this->_update_count($content_id); - return $fcid; - } - - /** - * A low-level method to unflag content. - * - * You probably shouldn't call this raw private method: call the flag() - * function instead. - * - * @return - * If the content was flagged, returns the value of the now deleted 'fcid' - * column. Else, returns FALSE. - * - * @private - */ - function _unflag($content_id, $uid, $sid) { - $fcid = db_select('flag_content', 'fc') - ->fields('fc', array('fcid')) - ->condition('fid', $this->fid) - ->condition('uid', $uid) - ->condition('sid', $sid) - ->condition('content_id', $content_id) - ->execute() - ->fetchField(); - if ($fcid) { - db_delete('flag_content')->condition('fcid', $fcid)->execute(); - $this->_update_count($content_id); - } - return $fcid; - } - - /** - * Updates the flag count for this content - * - * @private - */ - function _update_count($content_id) { - $count = db_select('flag_content', 'fc')->fields('fc', array('fcid')) - ->condition('fid', $this->fid) - ->condition('content_id', $content_id) - ->countQuery()->execute()->fetchField(); - if ($count == 0) { - db_delete('flag_counts')->condition('fid', $this->fid)->condition('content_id', $content_id)->execute(); - } - else { - $num_updated = db_update('flag_counts')->fields(array('count' => $count)) - ->condition('fid', $this->fid) - ->condition('content_id', $content_id) - ->execute(); - if (!$num_updated) { - db_insert('flag_counts')->fields(array( - 'fid' => $this->fid, - 'content_type' => $this->content_type, - 'content_id' => $content_id, - 'count' => $count)) - ->execute(); - } - } - } - - /** - * Set a cookie for anonymous users to record their flagging. - * - * @private - */ - function _flag_anonymous($content_id) { - $storage = FlagCookieStorage::factory($this); - $storage->flag($content_id); - } - - /** - * Remove the cookie for anonymous users to record their unflagging. - * - * @private - */ - function _unflag_anonymous($content_id) { - $storage = FlagCookieStorage::factory($this); - $storage->unflag($content_id); - } - - /** - * Returns the number of times an item is flagged. - * - * Thanks to using a cache, inquiring several different flags about the same - * item results in only one SQL query. - */ - function get_count($content_id) { - $counts = flag_get_counts($this->content_type, $content_id); - return isset($counts[$this->name]) ? $counts[$this->name] : 0; - } - - /** - * Returns the number of items a user has flagged. - * - * For global flags, pass '0' as the user ID and session ID. - */ - function get_user_count($uid, $sid = NULL) { - if (!isset($sid)) { - $sid = flag_get_sid($uid); - } - return db_select('flag_content', 'fc')->fields('fc', array('fcid')) - ->condition('fid', $this->fid)->condition('uid', $uid)->condition('sid', $sid) - ->countQuery()->execute(); - } - - /** - * Processes a flag label for display. This means language translation and - * token replacements. - * - * You should always call this function and not get at the label directly. - * E.g., do `print $flag->get_label('title')` instead of `print - * $flag->title`. - * - * @param $label - * The label to get, e.g. 'title', 'flag_short', 'unflag_short', etc. - * @param $content_id - * The ID in whose context to interpret tokens. If not given, only global - * tokens will be substituted. - * @return - * The processed label. - */ - function get_label($label, $content_id = NULL) { - if (!isset($this->$label)) { - return; - } - $label = t($this->$label); - if (strpos($label, '[') !== FALSE) { - $label = $this->replace_tokens($label, array(), array('sanitize' => FALSE), $content_id); - } - return filter_xss_admin($label); - } - - /** - * Get the link type for this flag. - */ - function get_link_type() { - $link_types = flag_get_link_types(); - return (isset($this->link_type) && isset($link_types[$this->link_type])) ? $link_types[$this->link_type] : $link_types['normal']; - } - - /** - * Replaces tokens in a label. Only the 'global' token context is recognized - * by default, so derived classes should override this method to add all - * token contexts they understand. - */ - function replace_tokens($label, $contexts, $options, $content_id) { - return token_replace($label, $contexts, $options); - } - - /** - * Returns the token types this flag understands in labels. These are used - * for narrowing down the token list shown in the help box to only the - * relevant ones. - * - * Derived classes should override this. - */ - function get_labels_token_types() { - return array(); - } - - /** - * A convenience method for getting the flag title. - * - * `$flag->get_title()` is shorthand for `$flag->get_label('title')`. - */ - function get_title($content_id = NULL, $reset = FALSE) { - static $titles = array(); - if ($reset) { - $titles = array(); - } - $slot = intval($content_id); // Convert NULL to 0. - if (!isset($titles[$this->fid][$slot])) { - $titles[$this->fid][$slot] = $this->get_label('title', $content_id); - } - return $titles[$this->fid][$slot]; - } - - /** - * Returns a 'flag action' object. It exists only for the sake of its - * informative tokens. Currently, it's utilized only for the 'mail' action. - * - * Derived classes should populate the 'content_title' and 'content_url' - * slots. - */ - function get_flag_action($content_id) { - $flag_action = new stdClass(); - $flag_action->flag = $this->name; - $flag_action->content_type = $this->content_type; - $flag_action->content_id = $content_id; - return $flag_action; - } - - /** - * @addtogroup actions - * @{ - * Methods that can be overridden to support Actions. - */ - - /** - * Returns an array of all actions that are executable with this flag. - */ - function get_valid_actions() { - $actions = module_invoke_all('action_info'); - foreach ($actions as $callback => $action) { - if ($action['type'] != $this->content_type && !in_array('any', $action['triggers'])) { - unset($actions[$callback]); - } - } - return $actions; - } - - /** - * Returns objects the action may possibly need. This method should return at - * least the 'primary' object the action operates on. - * - * This method is needed because get_valid_actions() returns actions that - * don't necessarily operate on an object of a type this flag manages. For - * example, flagging a comment may trigger an 'Unpublish post' action on a - * node; So the comment flag needs to tell the action about some node. - * - * Derived classes must implement this. - * - * @abstract - */ - function get_relevant_action_objects($content_id) { - return array(); - } - - /** - * @} End of "addtogroup actions". - */ - - /** - * @addtogroup views - * @{ - * Methods that can be overridden to support the Views module. - */ - - /** - * Returns information needed for Views integration. E.g., the Views table - * holding the flagged content, its primary key, and various labels. See - * derived classes for examples. - * - * @static - */ - function get_views_info() { - return array(); - } - - /** - * @} End of "addtogroup views". - */ - - /** - * Saves a flag to the database. It is a wrapper around update() and insert(). - */ - function save() { - if (isset($this->fid)) { - $this->update(); - $this->is_new = FALSE; - } - else { - $this->insert(); - $this->is_new = TRUE; - } - // Clear the page cache for anonymous users. - cache_clear_all('*', 'cache_page', TRUE); - } - - /** - * Saves an existing flag to the database. Better use save(). - */ - function update() { - db_update('flags')->fields(array( - 'name' => $this->name, - 'title' => $this->title, - 'global' => $this->global, - 'options' => $this->get_serialized_options())) - ->condition('fid', $this->fid) - ->execute(); - db_delete('flag_types')->condition('fid', $this->fid)->execute(); - foreach ($this->types as $type) { - db_insert('flag_types')->fields(array( - 'fid' => $this->fid, - 'type' => $type)) - ->execute(); - } - } - - /** - * Saves a new flag to the database. Better use save(). - */ - function insert() { - $this->fid = db_insert('flags') - ->fields(array( - 'content_type' => $this->content_type, - 'name' => $this->name, - 'title' => $this->title, - 'global' => $this->global, - 'options' => $this->get_serialized_options(), - )) - ->execute(); - foreach ($this->types as $type) { - db_insert('flag_types') - ->fields(array( - 'fid' => $this->fid, - 'type' => $type, - )) - ->execute(); - } - } - - /** - * Options are stored serialized in the database. - */ - function get_serialized_options() { - $option_names = array_keys($this->options()); - $options = array(); - foreach ($option_names as $option) { - $options[$option] = $this->$option; - } - return serialize($options); - } - - /** - * Deletes a flag from the database. - */ - function delete() { - db_delete('flags')->condition('fid', $this->fid)->execute(); - db_delete('flag_content')->condition('fid', $this->fid)->execute(); - db_delete('flag_types')->condition('fid', $this->fid)->execute(); - db_delete('flag_counts')->condition('fid', $this->fid)->execute(); - module_invoke_all('flag_delete', $this); - } - - /** - * Returns TRUE if this flag's declared API version is compatible with this - * module. - * - * An "incompatible" flag is one exported (and now being imported or exposed - * via hook_flag_default_flags()) by a different version of the Flag module. - * An incompatible flag should be treated as a "black box": it should not be - * saved or exported because our code may not know to handle its internal - * structure. - */ - function is_compatible() { - if (isset($this->fid)) { - // Database flags are always compatible. - return TRUE; - } - else { - if (!isset($this->api_version)) { - $this->api_version = 1; - } - return $this->api_version == FLAG_API_VERSION; - } - } - - /** - * Finds the "default flag" corresponding to this flag. - * - * Flags defined in code ("default flags") can be overridden. This method - * returns the default flag that is being overridden by $this. Returns NULL - * if $this overrides no default flag. - */ - function find_default_flag() { - if ($this->fid) { - $default_flags = flag_get_default_flags(TRUE); - if (isset($default_flags[$this->name])) { - return $default_flags[$this->name]; - } - } - } - - /** - * Reverts an overriding flag to its default state. - * - * Note that $this isn't altered. To see the reverted flag you'll have to - * call flag_get_flag($this->name) again. - * - * @return - * TRUE if the flag was reverted successfully; FALSE if there was an error; - * NULL if this flag overrides no default flag. - */ - function revert() { - if (($default_flag = $this->find_default_flag())) { - if ($default_flag->is_compatible()) { - $default_flag = clone $default_flag; - $default_flag->fid = $this->fid; - $default_flag->save(); - flag_get_flags(NULL, NULL, NULL, TRUE); - return TRUE; - } - else { - return FALSE; - } - } - } - - /** - * Disable a flag provided by a module. - */ - function disable() { - if (isset($this->module)) { - $flag_status = variable_get('flag_default_flag_status', array()); - $flag_status[$this->name] = FALSE; - variable_set('flag_default_flag_status', $flag_status); - } - } - - /** - * Enable a flag provided by a module. - */ - function enable() { - if (isset($this->module)) { - $flag_status = variable_get('flag_default_flag_status', array()); - $flag_status[$this->name] = TRUE; - variable_set('flag_default_flag_status', $flag_status); - } - } - - /** - * Returns administrative menu path for carrying out some action. - */ - function admin_path($action) { - if ($action == 'edit') { - // Since 'edit' is the default tab, we omit the action. - return FLAG_ADMIN_PATH . '/manage/' . $this->name; - } - else { - return FLAG_ADMIN_PATH . '/manage/' . $this->name . '/' . $action; - } - } - - /** - * Renders a flag/unflag link. This is a wrapper around theme('flag') that, - * in Drupal 6, easily channels the call to the right template file. - * - * For parameters docmentation, see theme_flag(). - */ - function theme($action, $content_id, $after_flagging = FALSE) { - static $js_added = array(); - global $user; - - // If the flagging user is anonymous, set a boolean for the benefit of - // JavaScript code. Currently, only our "anti-crawlers" mechanism uses it. - if ($user->uid == 0 && !isset($js_added['anonymous'])) { - $js_added['anonymous'] = TRUE; - drupal_add_js(array('flag' => array('anonymous' => TRUE)), 'setting'); - } - - // If the flagging user is anonymous and the page cache is enabled, we - // update the links through JavaScript. - if ($this->uses_anonymous_cookies() && !$after_flagging) { - if ($this->global) { - // In case of global flags, the JavaScript template is to contain - // the opposite of the current state. - $js_action = ($action == 'flag' ? 'unflag' : 'flag'); - } - else { - // In case of non-global flags, we always show the "flag!" link, - // and then replace it with the "unflag!" link through JavaScript. - $js_action = 'unflag'; - $action = 'flag'; - } - if (!isset($js_added[$this->name . '_' . $content_id])) { - $js_added[$this->name . '_' . $content_id] = TRUE; - $js_template = theme($this->theme_suggestions(), array('flag' => $this, 'action' => $js_action, 'content_id' => $content_id, 'after_flagging' => $after_flagging)); - drupal_add_js(array('flag' => array('templates' => array($this->name . '_' . $content_id => $js_template))), 'setting'); - } - } - - return theme($this->theme_suggestions(), array('flag' => $this, 'action' => $action, 'content_id' => $content_id, 'after_flagging' => $after_flagging)); - } - - /** - * Provides an array of possible themes to try for a given flag. - */ - function theme_suggestions() { - $suggestions = array(); - $suggestions[] = 'flag__' . $this->name; - $suggestions[] = 'flag'; - return $suggestions; - } -} - -/** - * Implements a node flag. - */ -class flag_node extends flag_flag { - function options() { - $options = parent::options(); - $options += array( - 'show_on_page' => TRUE, - 'show_on_teaser' => TRUE, - 'show_on_form' => FALSE, - 'access_author' => '', - 'i18n' => 0, - ); - return $options; - } - - function options_form(&$form) { - parent::options_form($form); - // Support for i18n flagging requires Translation helpers module. - $form['i18n'] = array( - '#type' => 'radios', - '#title' => t('Internationalization'), - '#options' => array( - '1' => t('Flag translations of content as a group'), - '0' => t('Flag each translation of content separately'), - ), - '#default_value' => $this->i18n, - '#description' => t('Flagging translations as a group effectively allows users to flag the original piece of content regardless of the translation they are viewing. Changing this setting will not update content that has been flagged already.'), - '#access' => module_exists('translation_helpers'), - '#weight' => 5, - ); - - $form['access']['access_author'] = array( - '#type' => 'radios', - '#title' => t('Flag access by content authorship'), - '#options' => array( - '' => t('No additional restrictions'), - 'own' => t('Users may only flag content they own'), - 'others' => t('Users may only flag content of others'), - ), - '#default_value' => $this->access_author, - '#description' => t("Restrict access to this flag based on the user's ownership of the content. Users must also have access to the flag through the role settings."), - ); - - $form['display']['show_on_teaser'] = array( - '#type' => 'checkbox', - '#title' => t('Display link on node teaser'), - '#default_value' => $this->show_on_teaser, - '#access' => empty($this->locked['show_on_teaser']), - ); - $form['display']['show_on_page'] = array( - '#type' => 'checkbox', - '#title' => t('Display link on node page'), - '#default_value' => $this->show_on_page, - '#access' => empty($this->locked['show_on_page']), - ); - $form['display']['show_on_form'] = array( - '#type' => 'checkbox', - '#title' => t('Display checkbox on node edit form'), - '#default_value' => $this->show_on_form, - '#description' => t('If you elect to have a checkbox on the node edit form, you may specify its initial state in the settings form for each content type.', array('@content-types-url' => url('admin/structure/types'))), - '#access' => empty($this->locked['show_on_form']), - ); - } - - function _load_content($content_id) { - return is_numeric($content_id) ? node_load($content_id) : NULL; - } - - function applies_to_content_object($node) { - if ($node && in_array($node->type, $this->types)) { - return TRUE; - } - return FALSE; - } - - function access_multiple($content_ids, $account = NULL) { - $access = parent::access_multiple($content_ids, $account); - - // Ensure that only flaggable node types are granted access. This avoids a - // node_load() on every type, usually done by applies_to_content_id(). - $result = db_select('node', 'n')->fields('n', array('nid')) - ->condition('nid', $content_ids, 'IN') - ->condition('type', $this->types, 'NOT IN') - ->execute(); - foreach ($result as $row) { - $access[$row->nid] = FALSE; - } - - return $access; - } - - function get_content_id($node) { - return $node->nid; - } - - /** - * Adjust the Content ID to find the translation parent if i18n-enabled. - * - * @param $content_id - * The nid for the content. - * @return - * The tnid if available, the nid otherwise. - */ - function get_translation_id($content_id) { - if ($this->i18n) { - $node = $this->fetch_content($content_id); - if (!empty($node->tnid)) { - $content_id = $node->tnid; - } - } - return $content_id; - } - - function uses_hook_link($teaser) { - if ($teaser && $this->show_on_teaser || !$teaser && $this->show_on_page) { - return TRUE; - } - return FALSE; - } - - function flag($action, $content_id, $account = NULL, $skip_permission_check = FALSE) { - $content_id = $this->get_translation_id($content_id); - return parent::flag($action, $content_id, $account, $skip_permission_check); - } - - // Instead of overriding is_flagged() we override get_flagging_record(), - // which is the underlying method. - function get_flagging_record($content_id, $uid = NULL, $sid = NULL) { - $content_id = $this->get_translation_id($content_id); - return parent::get_flagging_record($content_id, $uid, $sid); - } - - function get_labels_token_types() { - return array_merge(array('node'), parent::get_labels_token_types()); - } - - function replace_tokens($label, $contexts, $options, $content_id) { - if (is_numeric($content_id) && ($node = $this->fetch_content($content_id))) { - $contexts['node'] = $node; - } - // Nodes accept the node-type as a $content_id in the case that a new node - // is being created and a full node object does not yet exist. - elseif (!empty($content_id) && ($type = node_type_get_type($content_id))) { - $content_id = NULL; - $contexts['node'] = (object) array( - 'nid' => NULL, - 'type' => $type->type, - 'title' => '', - ); - } - return parent::replace_tokens($label, $contexts, $options, $content_id); - } - - function get_flag_action($content_id) { - $flag_action = parent::get_flag_action($content_id); - $node = $this->fetch_content($content_id); - $flag_action->content_title = $node->title; - $flag_action->content_url = _flag_url('node/' . $node->nid); - return $flag_action; - } - - function get_valid_actions() { - $actions = module_invoke_all('action_info'); - foreach ($actions as $callback => $action) { - if ($action['type'] != 'node' && !in_array('any', $action['triggers'])) { - unset($actions[$callback]); - } - } - return $actions; - } - - function get_relevant_action_objects($content_id) { - return array( - 'node' => $this->fetch_content($content_id), - ); - } - - function get_views_info() { - return array( - 'views table' => 'node', - 'join field' => 'nid', - 'title field' => 'title', - 'title' => t('Node flag'), - 'help' => t('Limit results to only those nodes flagged by a certain flag; Or display information about the flag set on a node.'), - 'counter title' => t('Node flag counter'), - 'counter help' => t('Include this to gain access to the flag counter field.'), - ); - } -} - -/** - * Implements a comment flag. - */ -class flag_comment extends flag_flag { - function options() { - $options = parent::options(); - $options += array( - 'access_author' => '', - 'show_on_comment' => TRUE, - ); - return $options; - } - - function options_form(&$form) { - parent::options_form($form); - - $form['access']['access_author'] = array( - '#type' => 'radios', - '#title' => t('Flag access by content authorship'), - '#options' => array( - '' => t('No additional restrictions'), - 'comment_own' => t('Users may only flag own comments'), - 'comment_others' => t('Users may only flag comments by others'), - 'node_own' => t('Users may only flag comments of nodes they own'), - 'node_others' => t('Users may only flag comments of nodes by others'), - ), - '#default_value' => $this->access_author, - '#description' => t("Restrict access to this flag based on the user's ownership of the content. Users must also have access to the flag through the role settings."), - ); - - $form['display']['show_on_comment'] = array( - '#type' => 'checkbox', - '#title' => t('Display link under comment'), - '#default_value' => $this->show_on_comment, - '#access' => empty($this->locked['show_on_comment']), - ); - } - - function _load_content($content_id) { - return comment_load($content_id); - } - - function applies_to_content_object($comment) { - if ($comment && ($node = node_load($comment->nid)) && in_array($node->type, $this->types)) { - return TRUE; - } - return FALSE; - } - - function access_multiple($content_ids, $account = NULL) { - $account = isset($account) ? $account : $GLOBALS['user']; - $access = parent::access_multiple($content_ids, $account); - - // Ensure node types are granted access. This avoids a - // node_load() on every type, usually done by applies_to_content_id(). - $query = db_select('comment', 'c'); - $query->innerJoin('node', 'n', 'c.nid = n.nid'); - $result = $query - ->fields('c', array('cid')) - ->condition('c.cid', $content_ids, 'IN') - ->condition('n.type', $this->types, 'NOT IN') - ->execute(); - foreach ($result as $row) { - $access[$row->nid] = FALSE; - } - - return $access; - } - - function get_content_id($comment) { - // Store the comment object in the static cache, to avoid getting it - // again unneedlessly. - $this->remember_content($comment->cid, $comment); - return $comment->cid; - } - - function uses_hook_link($teaser) { - return $this->show_on_comment; - } - - function get_labels_token_types() { - return array_merge(array('comment', 'node'), parent::get_labels_token_types()); - } - - function replace_tokens($label, $contexts, $options, $content_id) { - if ($content_id) { - if (($comment = $this->fetch_content($content_id)) && ($node = node_load($comment->nid))) { - $contexts['node'] = $node; - $contexts['comment'] = $comment; - } - } - return parent::replace_tokens($label, $contexts, $options, $content_id); - } - - function get_flag_action($content_id) { - $flag_action = parent::get_flag_action($content_id); - $comment = $this->fetch_content($content_id); - $flag_action->content_title = $comment->subject; - $flag_action->content_url = _flag_url("comment/$comment->cid", "comment-$comment->cid"); - return $flag_action; - } - - function get_relevant_action_objects($content_id) { - $comment = $this->fetch_content($content_id); - return array( - 'comment' => $comment, - 'node' => node_load($comment->nid), - ); - } - - function get_views_info() { - return array( - 'views table' => 'comment', - 'join field' => 'cid', - 'title field' => 'subject', - 'title' => t('Comment flag'), - 'help' => t('Limit results to only those comments flagged by a certain flag; Or display information about the flag set on a comment.'), - 'counter title' => t('Comment flag counter'), - 'counter help' => t('Include this to gain access to the flag counter field.'), - ); - } -} - -/** - * Implements a user flag. - */ -class flag_user extends flag_flag { - function options() { - $options = parent::options(); - $options += array( - 'show_on_profile' => TRUE, - 'access_uid' => '', - ); - return $options; - } - - function options_form(&$form) { - parent::options_form($form); - $form['access']['types'] = array( - // A user flag doesn't support node types. - // TODO: Maybe support roles instead of node types. - '#type' => 'value', - '#value' => array(0 => 0), - ); - $form['access']['access_uid'] = array( - '#type' => 'checkbox', - '#title' => t('Users may flag themselves'), - '#description' => t('Disabling this option may be useful when setting up a "friend" flag, when a user flagging themself does not make sense.'), - '#default_value' => $this->access_uid ? 0 : 1, - ); - $form['display']['show_on_profile'] = array( - '#type' => 'checkbox', - '#title' => t('Display link on user profile page'), - '#default_value' => $this->show_on_profile, - '#access' => empty($this->locked['show_on_profile']), - ); - } - - function form_input($form_values) { - parent::form_input($form_values); - // The access_uid value is intentionally backwards from the UI, to avoid - // confusion caused by checking a box to disable a feature. - $this->access_uid = empty($form_values['access_uid']) ? 'others' : ''; - } - - function _load_content($content_id) { - return user_load($content_id); - } - - function applies_to_content_object($user) { - // This user flag doesn't currently support subtypes so we return TRUE for - // any user. - if ($user) { - return TRUE; - } - return FALSE; - } - - function access($content_id, $action = NULL, $account = NULL) { - $access = parent::access($content_id, $action, $account); - $account = isset($account) ? $account : $GLOBALS['user']; - - // Prevent users from flagging themselves. - if ($this->access_uid == 'others' && $content_id == $account->uid) { - $access = FALSE; - } - - return $access; - } - - function access_multiple($content_ids, $account = NULL) { - $account = isset($account) ? $account : $GLOBALS['user']; - $access = parent::access_multiple($content_ids, $account); - - // Exclude anonymous. - if (array_key_exists(0, $access)) { - $access[0] = FALSE; - } - - // Prevent users from flagging themselves. - if ($this->access_uid == 'others' && array_key_exists($account->uid, $access)) { - $access[$account->uid] = FALSE; - } - - return $access; - } - - function get_content_id($user) { - return $user->uid; - } - - function uses_hook_link($teaser) { - if ($this->show_on_profile) { - return TRUE; - } - return FALSE; - } - - function get_labels_token_types() { - return array_merge(array('user'), parent::get_labels_token_types()); - } - - function replace_tokens($label, $contexts, $options, $content_id) { - if ($content_id && ($user = $this->fetch_content($content_id))) { - $contexts['user'] = $user; - } - return parent::replace_tokens($label, $contexts, $options, $content_id); - } - - function get_flag_action($content_id) { - $flag_action = parent::get_flag_action($content_id); - $user = $this->fetch_content($content_id); - $flag_action->content_title = $user->name; - $flag_action->content_url = _flag_url('user/' . $user->uid); - return $flag_action; - } - - function get_relevant_action_objects($content_id) { - return array( - 'user' => $this->fetch_content($content_id), - ); - } - - function get_views_info() { - return array( - 'views table' => 'users', - 'join field' => 'uid', - 'title field' => 'name', - 'title' => t('User flag'), - 'help' => t('Limit results to only those users flagged by a certain flag; Or display information about the flag set on a user.'), - 'counter title' => t('User flag counter'), - 'counter help' => t('Include this to gain access to the flag counter field.'), - ); - } -} - -/** - * A dummy flag to be used where the real implementation can't be found. - */ -class flag_broken extends flag_flag { - function options_form(&$form) { - $form = array(); - $form['error'] = array( - '#markup' => '
' . t("The module providing this flag wasn't found, or this flag type, %type, isn't valid.", array('%type' => $this->content_type)) . '
', - ); - } -} - -/** - * A shortcut function to output the link URL. - */ -function _flag_url($path, $fragment = NULL, $absolute = TRUE) { - return url($path, array('fragment' => $fragment, 'absolute' => $absolute)); -} - - -/** - * Utility class to handle cookies. - * - * Cookies are used to record flaggings for anonymous users on cached pages. - * - * This class contains only two instance methods. Usage example: - * @code - * $storage = FlagCookieStorage::factory($flag); - * $storage->flag(145); - * $storage->unflag(17); - * @endcode - * - * You may delete all the cookies with FlagCookieStorage::drop(). - */ -abstract class FlagCookieStorage { - - /** - * Returns the actual storage object compatible with the flag. - */ - static function factory($flag) { - if ($flag->global) { - return new FlagGlobalCookieStorage($flag); - } - else { - return new FlagNonGlobalCookieStorage($flag); - } - } - - function __construct($flag) { - $this->flag = $flag; - } - - /** - * "Flags" an item. - * - * It just records this fact in a cookie. - */ - abstract function flag($content_id); - - /** - * "Unflags" an item. - * - * It just records this fact in a cookie. - */ - abstract function unflag($content_id); - - /** - * Deletes all the cookies. - * - * (Etymology: "drop" as in "drop database".) - */ - static function drop() { - FlagGlobalCookieStorage::drop(); - FlagNonGlobalCookieStorage::drop(); - } -} - -/** - * Storage handler for global flags. - */ -class FlagGlobalCookieStorage extends FlagCookieStorage { - - function flag($content_id) { - $cookie_key = $this->cookie_key($content_id); - setcookie($cookie_key, 1, REQUEST_TIME + $this->get_lifetime(), base_path()); - $_COOKIE[$cookie_key] = 1; - } - - function unflag($content_id) { - $cookie_key = $this->cookie_key($content_id); - setcookie($cookie_key, 0, REQUEST_TIME + $this->get_lifetime(), base_path()); - $_COOKIE[$cookie_key] = 0; - } - - // Global flags persist for the length of the minimum cache lifetime. - protected function get_lifetime() { - $cookie_lifetime = variable_get('cache', 0) ? variable_get('cache_lifetime', 0) : -1; - // Do not let the cookie lifetime be 0 (which is the no cache limit on - // anonymous page caching), since it would expire immediately. Usually - // the no cache limit means caches are cleared on cron, which usually runs - // at least once an hour. - if ($cookie_lifetime == 0) { - $cookie_lifetime = 3600; - } - return $cookie_lifetime; - } - - protected function cookie_key($content_id) { - return 'flag_global_' . $this->flag->name . '_' . $content_id; - } - - /** - * Deletes all the global cookies. - */ - static function drop() { - foreach ($_COOKIE as $key => $value) { - if (strpos($key, 'flag_global_') === 0) { - setcookie($key, FALSE, 0, base_path()); - unset($_COOKIE[$key]); - } - } - } -} - -/** - * Storage handler for non-global flags. - */ -class FlagNonGlobalCookieStorage extends FlagCookieStorage { - - // The anonymous per-user flaggings are stored in a single cookie, so that - // all of them persist as long as the Drupal cookie lifetime. - - function __construct($flag) { - parent::__construct($flag); - $this->flaggings = isset($_COOKIE['flags']) ? explode(' ', $_COOKIE['flags']) : array(); - } - - function flag($content_id) { - if (!$this->is_flagged($content_id)) { - $this->flaggings[] = $this->cookie_key($content_id); - $this->write(); - } - } - - function unflag($content_id) { - if (($index = $this->index_of($content_id)) !== FALSE) { - unset($this->flaggings[$index]); - $this->write(); - } - } - - protected function get_lifetime() { - return min((int) ini_get('session.cookie_lifetime'), (int) ini_get('session.gc_maxlifetime')); - } - - protected function cookie_key($content_id) { - return $this->flag->name . '_' . $content_id; - } - - protected function write() { - $serialized = implode(' ', array_filter($this->flaggings)); - setcookie('flags', $serialized, REQUEST_TIME + $this->get_lifetime(), base_path()); - $_COOKIE['flags'] = $serialized; - } - - protected function is_flagged($content_id) { - return $this->index_of($content_id) !== FALSE; - } - - protected function index_of($content_id) { - return array_search($this->cookie_key($content_id), $this->flaggings); - } - - /** - * Deletes the cookie. - */ - static function drop() { - if (isset($_COOKIE['flags'])) { - setcookie('flags', FALSE, 0, base_path()); - unset($_COOKIE['flags']); - } - } -} - diff --git a/flag.info b/flag.info deleted file mode 100644 index 6933238..0000000 --- a/flag.info +++ /dev/null @@ -1,15 +0,0 @@ -name = Flag -description = Create customized flags that users can set on content. -core = 7.x -package = Flags -configure = admin/structure/flags - -; Files that contain classes. -files[] = flag.inc -files[] = flag.rules.inc -files[] = includes/flag_handler_argument_content_id.inc -files[] = includes/flag_handler_field_ops.inc -files[] = includes/flag_handler_filter_flagged.inc -files[] = includes/flag_handler_relationships.inc -files[] = includes/flag_plugin_argument_validate_flaggability.inc -files[] = tests/flag.test diff --git a/flag.info.yml b/flag.info.yml new file mode 100644 index 0000000..d62a74d --- /dev/null +++ b/flag.info.yml @@ -0,0 +1,8 @@ +name: Flag +description: Create customized flags that users can set on entities. +core: 8.x +type: module +package: Flags +configure: admin/structure/flags +dependencies: + - views_ui diff --git a/flag.install b/flag.install index 7ebdf11..52e6bf5 100644 --- a/flag.install +++ b/flag.install @@ -6,377 +6,96 @@ */ /** - * Implements hook_install(). + * Implements hook_schema(). */ -function flag_install() { -} - -/** - * Implementation of hook_uninstall(). - */ -function flag_uninstall() { - $result = db_select('variable', 'v') - ->fields('v', array('name')) - ->condition('name', 'flag_%', 'LIKE') - ->execute(); - foreach ($result as $row) { - variable_del($row->name); - } - - drupal_set_message(t('Flag has been uninstalled.')); -} +function flag_schema() { + $schema = []; -/** - * Implementation of hook_enable(). - * - * We create the demonstration flag on enable, so hook implementations in flag - * module will fire correctly, as the APIs are not available on install. - */ -function flag_enable() { - // Load the flag API in case we want to use it when enabling. - include_once(drupal_get_path('module', 'flag') .'/flag.module'); + $schema['flag_counts'] = [ + 'description' => 'The number of times an item has been flagged.', + 'fields' => [ + 'fid' => [ + 'type' => 'varchar', + 'length' => '32', + ], + 'entity_type' => [ + 'description' => 'The flag type, usually one of "node", "comment", "user".', + 'type' => 'varchar', + 'length' => '128', + ], + 'entity_id' => [ + 'description' => 'The unique ID of the content, usually either the {cid}, {uid}, or {nid}.', + 'type' => 'int', + 'unsigned' => TRUE, + 'disp-width' => '10', + ], + 'count' => [ + 'description' => 'The number of times this object has been flagged for this flag.', + 'type' => 'int', + 'unsigned' => TRUE, + 'disp-width' => '10', + ], + 'last_updated' => [ + 'description' => 'The UNIX time stamp representing when the flag was last updated.', + 'type' => 'int', + 'unsigned' => TRUE, + 'disp-size' => 11, + ], + ], + 'primary key' => ['fid', 'entity_id'], + 'indexes' => [ + 'fid_entity_type' => ['fid', 'entity_type'], + 'entity_type_entity_id' => ['entity_type', 'entity_id'], + 'fid_count' => ['fid', 'count'], + 'fid_last_updated' => ['fid', 'last_updated'], + ], + ]; - if (!flag_get_flags()) { - // Install a demonstration flag only if no flag exists. This is to prevent - // a case where a disables and enables the module, and the demonstration - // flag is overwritten or re-created. - $flag = flag_flag::factory_by_content_type('node'); - $configuration = array( - 'name' => 'bookmarks', - 'global' => 0, - 'show_on_page' => 1, - 'show_on_teaser' => 1, - 'show_on_form' => 1, - // The following UI labels aren't wrapped in t() because they are written - // to the DB in English. They are passed to t() later, thus allowing for - // multilingual sites. - 'title' => 'Bookmarks', - 'flag_short' => 'Bookmark this', - 'flag_long' => 'Add this post to your bookmarks', - 'flag_message' => 'This post has been added to your bookmarks', - 'unflag_short' => 'Unbookmark this', - 'unflag_long' => 'Remove this post from your bookmarks', - 'unflag_message' => 'This post has been removed from your bookmarks', - 'types' => _flag_install_get_suggested_node_types(), - ); - $flag->form_input($configuration); - $flag->save(); - } + return $schema; } /** - * Returns some node types to which the demonstration 'bookmarks' flag will apply. + * Implements hook_uninstall(). */ -function _flag_install_get_suggested_node_types() { - $preferred = array('article', 'story', 'forum', 'blog'); - $existing = array_intersect($preferred, array_keys(node_type_get_types())); - if (!$existing) { - // As a last resort, take the first preference. - return array($preferred[0]); - } - return $existing; +function flag_uninstall() { + drupal_set_message(t('Flag has been uninstalled.')); } /** - * Implementation of hook_requirements(). - * - * Prevent installing this module if the "Flag content" module is installed as well. + * Implements hook_requirements(). */ function flag_requirements($phase) { - $requirements = array(); - $t = get_t(); - if ($phase == 'install') { - if (!defined('MAINTENANCE_MODE') && _flag_flag_content_installed()) { - $requirements['flag_content_clash'] = array( - 'title' => $t('Flag'), - 'severity' => REQUIREMENT_ERROR, - 'description' => _flag_flag_content_message(), - ); - } - } + $requirements = array(); + /* if ($phase == 'runtime') { - if (module_exists('translation') && !module_exists('translation_helpers')) { + if (\Drupal::moduleHandler()->moduleExists('translation') && !\Drupal::moduleHandler()->moduleExists('translation_helpers')) { $requirements['flag_translation'] = array( - 'title' => $t('Flag'), + 'title' => t('Flag'), 'severity' => REQUIREMENT_ERROR, - 'description' => $t('To have the flag module work with translations, you need to install and enable the Translation helpers module.'), - 'value' => $t('Translation helpers module not found.'), + 'description' => t('To have the flag module work with translations, you need to install and enable the Translation helpers module.'), + 'value' => t('Translation helpers module not found.'), ); } - if (module_exists('session_api')) { + if (\Drupal::moduleHandler()->moduleExists('session_api')) { if (file_exists('./robots.txt')) { $flag_path = url('flag') . '/'; + // We don't use url() because this may return an absolute URL when + // language negotiation is set to 'domain'. + $flag_path = parse_url($flag_path, PHP_URL_PATH); $robots_string = 'Disallow: ' . $flag_path; $contents = file_get_contents('./robots.txt'); if (strpos($contents, $robots_string) === FALSE) { $requirements['flag_robots'] = array( - 'title' => $t('Flag robots.txt problem'), + 'title' => t('Flag robots.txt problem'), 'severity' => REQUIREMENT_WARNING, - 'description' => $t('Flag module may currently be used with anonymous users, however the robots.txt file does not exlude the "@flag-path" path, which may cause search engines to randomly flag and unflag content when they index the site. It is highly recommended to add "@robots-string" to your robots.txt file (located in the root of your Drupal installation).', array('@flag-path' => $flag_path, '@robots-string' => $robots_string)), - 'value' => $t('Search engines flagging content'), + 'description' => t('Flag module may currently be used with anonymous users, however the robots.txt file does not exclude the "@flag-path" path, which may cause search engines to randomly flag and unflag content when they index the site. It is highly recommended to add "@robots-string" to your robots.txt file (located in the root of your Drupal installation).', array('@flag-path' => $flag_path, '@robots-string' => $robots_string)), + 'value' => t('Search engines flagging content'), ); } } } } + */ return $requirements; } - -/** - * Returns TRUE if the "Flag content" module, which we aren't compatible with, - * is installed. - */ -function _flag_flag_content_installed() { - $version = @drupal_get_installed_schema_version('flag_content', TRUE); - return (isset($version) && $version != SCHEMA_UNINSTALLED); -} - -function _flag_flag_content_message() { - $t = get_t(); - return $t("You are trying to install the Flag module. However, you have the \"Flag content\" module installed, and these two modules aren't compatible (because they happen to use a database table by the same name). To install the Flag module, you'll first have to disable and then uninstall the \"Flag content\" module.", array('@uninstall-url' => url('admin/modules/uninstall'))); -} - -/** - * Implementation of hook_schema(). - */ -function flag_schema() { - $schema = array(); - - $schema['flags'] = array( - 'fields' => array( - 'fid' => array( - 'type' => 'serial', - 'size' => 'small', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'content_type' => array( - 'type' => 'varchar', - 'length' => '32', - 'not null' => TRUE, - 'default' => '', - ), - 'name' => array( - 'type' => 'varchar', - 'length' => '32', - 'not null' => FALSE, - 'default' => '', - ), - 'title' => array( - 'type' => 'varchar', - 'length' => '255', - 'not null' => FALSE, - 'default' => '', - ), - 'global' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => FALSE, - 'default' => 0, - ), - 'options' => array( - 'type' => 'text', - 'not null' => FALSE, - ), - ), - 'primary key' => array('fid'), - 'unique keys' => array( - 'name' => array('name'), - ), - ); - - $schema['flag_content'] = array( - 'fields' => array( - 'fcid' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'fid' => array( - 'type' => 'int', - 'size' => 'small', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'content_type' => array( - 'type' => 'varchar', - 'length' => '32', - 'not null' => TRUE, - 'default' => '', - ), - 'content_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'uid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'sid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'timestamp' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'disp-size' => 11, - ) - ), - 'primary key' => array('fcid'), - 'unique keys' => array( - 'fid_content_id_uid_sid' => array('fid', 'content_id', 'uid', 'sid'), - ), - 'indexes' => array( - 'content_type_content_id' => array('content_type', 'content_id'), - 'content_type_uid_sid' => array('content_type', 'uid', 'sid'), - ), - ); - - $schema['flag_types'] = array( - 'fields' => array( - 'fid' => array( - 'type' => 'int', - 'size' => 'small', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'type' => array( - 'type' => 'varchar', - 'length' => '32', - 'not null' => TRUE, - 'default' => '') - ), - 'indexes' => array( - 'fid' => array('fid'), - ), - ); - - $schema['flag_counts'] = array( - 'fields' => array( - 'fid' => array( - 'type' => 'int', - 'size' => 'small', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'content_type' => array( - 'type' => 'varchar', - 'length' => '32', - 'not null' => TRUE, - 'default' => '', - ), - 'content_id' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'disp-width' => '10', - ), - 'count' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'disp-width' => '10', - ) - ), - 'primary key' => array('fid', 'content_id'), - 'indexes' => array( - 'fid_content_type' => array('fid', 'content_type'), - 'content_type_content_id' => array('content_type', 'content_id'), - 'count' => array('count'), - ), - ); - - return $schema; -} - -function flag_update_last_removed() { - return 6004; -} - -/** - * Convert role access to have separate "flag" and "unflag" permissions. - */ -function flag_update_6200() { - if (db_field_exists('flags', 'roles')) { - $result = db_select('flags', 'f') - ->fields('f') - ->execute(); - foreach ($result as $flag) { - $roles = array_filter(explode(',', $flag->roles)); - $options = unserialize($flag->options); - $options['roles'] = array( - 'flag' => $roles, - 'unflag' => $roles, - ); - db_update('flags') - ->fields(array( - 'options' => serialize($options), - )) - ->condition('fid', $flag->fid) - ->execute(); - } - db_drop_field('flags', 'roles'); - } -} - -/** - * Refine the indexes. - * - * The content type inclusion actually slowed down on unique key. And a count - * index would be helpful for sorting by counts. - */ -function flag_update_6201() { - // Remove "content type" from one key, see http://drupal.org/node/612602. - db_drop_unique_key('flag_content', 'fid_content_type_content_id_uid'); - db_add_unique_key('flag_content', 'fid_content_id_uid', array('fid', 'content_id', 'uid')); - - // Add a count index, see http://drupal.org/node/489610. - db_add_index('flag_counts', 'count', array('count')); -} - -/** - * Add the sid column and unique index on the flag_content table. - */ -function flag_update_6202() { - // Drop the keys affected by the addition of the SID column. - db_drop_unique_key('flag_content', 'fid_content_id_uid'); - db_drop_index('flag_content', 'content_type_uid'); - - // Add the column. - db_add_field('flag_content', 'sid', array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0)); - - // Re-add the removed keys. - db_add_unique_key('flag_content', 'fid_content_id_uid_sid', array('fid', 'content_id', 'uid', 'sid')); - db_add_index('flag_content', 'content_type_uid_sid', array('content_type', 'uid', 'sid')); -} - -/** - * Remove count = 0 rows from the count tables. - */ -function flag_update_6203() { - db_delete('flag_counts') - ->condition('count', 0) - ->execute(); -} - -/** - * Remove "content type" from the flag_counts primary key. - */ -function flag_update_6204() { - db_drop_primary_key('flag_counts'); - db_add_primary_key('flag_counts', array('fid', 'content_id')); -} diff --git a/flag.links.action.yml b/flag.links.action.yml new file mode 100644 index 0000000..ea4e830 --- /dev/null +++ b/flag.links.action.yml @@ -0,0 +1,5 @@ +flag_add_page_action: + route_name: flag.add_page + title: 'Add New Flag' + appears_on: + - flag.list \ No newline at end of file diff --git a/flag.links.menu.yml b/flag.links.menu.yml new file mode 100644 index 0000000..e4282e8 --- /dev/null +++ b/flag.links.menu.yml @@ -0,0 +1,5 @@ +flag.admin.structure.flag: + title: 'Flags' + description: 'Configure flags for marking content with arbitrary information (such as offensive or bookmarked).' + parent: system.admin_structure + route_name: flag.list \ No newline at end of file diff --git a/flag.module b/flag.module index b0728e8..e159c72 100644 --- a/flag.module +++ b/flag.module @@ -5,887 +5,354 @@ * The Flag module. */ -define('FLAG_API_VERSION', 2); +define('FLAG_API_VERSION', 3); define('FLAG_ADMIN_PATH', 'admin/structure/flags'); define('FLAG_ADMIN_PATH_START', 3); -include_once dirname(__FILE__) .'/flag.inc'; +use Drupal\Component\Utility\String; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\flag\Entity\Flag; +use Drupal\node\NodeInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\user\UserInterface; +use Drupal\Core\Form\FormStateInterface; /** - * Implements hook_menu(). - */ -function flag_menu() { - $items[FLAG_ADMIN_PATH] = array( - 'title' => 'Flags', - 'page callback' => 'flag_admin_page', - 'access callback' => 'user_access', - 'access arguments' => array('administer flags'), - 'description' => 'Configure flags for marking content with arbitrary information (such as offensive or bookmarked).', - 'file' => 'includes/flag.admin.inc', - 'type' => MENU_NORMAL_ITEM, - ); - $items[FLAG_ADMIN_PATH . '/list'] = array( - 'title' => 'List', - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'weight' => -10, - ); - $items[FLAG_ADMIN_PATH . '/add'] = array( - 'title' => 'Add a new flag', - 'page callback' => 'flag_add_page', - 'access callback' => 'user_access', - 'access arguments' => array('administer flags'), - 'file' => 'includes/flag.admin.inc', - 'type' => MENU_LOCAL_ACTION, - 'weight' => 1, - ); - $items[FLAG_ADMIN_PATH . '/import'] = array( - 'title' => 'Import', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_import_form'), - 'access arguments' => array('administer flags'), - 'file' => 'includes/flag.export.inc', - 'type' => MENU_LOCAL_TASK, - 'weight' => 2, - ); - $items[FLAG_ADMIN_PATH . '/export'] = array( - 'title' => 'Export', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_export_form'), - 'access arguments' => array('administer flags'), - 'file' => 'includes/flag.export.inc', - 'type' => MENU_LOCAL_TASK, - 'weight' => 3, - ); - - $items[FLAG_ADMIN_PATH . '/manage/%flag'] = array( - 'load arguments' => array(TRUE), // Allow for disabled flags. - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_form', FLAG_ADMIN_PATH_START + 1), - 'access callback' => 'user_access', - 'access arguments' => array('administer flags'), - 'file' => 'includes/flag.admin.inc', - 'type' => MENU_CALLBACK, - // Make the flag title the default title for descendant menu items. - 'title callback' => '_flag_menu_title', - 'title arguments' => array(FLAG_ADMIN_PATH_START + 1), - ); - $items[FLAG_ADMIN_PATH . '/manage/%flag/edit'] = array( - 'load arguments' => array(TRUE), // Allow for disabled flags. - 'title' => 'Edit flag', - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'weight' => -10, - ); - $items[FLAG_ADMIN_PATH . '/manage/%flag/export'] = array( - 'title' => 'Export', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_export_form', FLAG_ADMIN_PATH_START + 1), - 'access arguments' => array('administer flags'), - 'file' => 'includes/flag.export.inc', - 'type' => MENU_LOCAL_TASK, - 'weight' => 20, - ); - $items[FLAG_ADMIN_PATH . '/manage/%flag/delete'] = array( - 'title' => 'Delete flag', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_delete_confirm', FLAG_ADMIN_PATH_START + 1), - 'access callback' => 'user_access', - 'access arguments' => array('administer flags'), - 'file' => 'includes/flag.admin.inc', - 'type' => MENU_CALLBACK, - ); - $items[FLAG_ADMIN_PATH . '/manage/%flag/update'] = array( - 'load arguments' => array(TRUE), // Allow for disabled flags. - 'title' => 'Update', - 'page callback' => 'flag_update_page', - 'page arguments' => array(FLAG_ADMIN_PATH_START + 1), - 'access arguments' => array('administer flags'), - 'file' => 'includes/flag.export.inc', - 'type' => MENU_CALLBACK, - ); - - $items['flag/%/%flag/%'] = array( - 'title' => 'Flag', - 'page callback' => 'flag_page', - 'page arguments' => array(1, 2, 3), - 'access callback' => 'user_access', - 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, - ); - $items['flag/confirm/%/%flag/%'] = array( - 'title' => 'Flag confirm', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_confirm', 2, 3, 4), - 'access callback' => 'user_access', - 'access arguments' => array('access content'), - 'type' => MENU_CALLBACK, - ); - - return $items; -} - -/** - * Menu loader for '%flag' arguments. - * - * @param $include_disabled - * Whether to return a disabled flag too. Normally only enabled flags are - * returned. Some menu items operate on disabled flags and in this case - * you need to turn on this switch by doing - * 'load arguments' => array(TRUE) in your menu router. - * - * @return - * Either the flag object, or FALSE if none was found. + * Implements hook_help(). */ -function flag_load($flag_name, $include_disabled = FALSE) { - if (($flag = flag_get_flag($flag_name))) { - return $flag; - } - else { - // No enabled flag was found. Search among the disabled ones. - if ($include_disabled) { - $default_flags = flag_get_default_flags(TRUE); - if (isset($default_flags[$flag_name])) { - return $default_flags[$flag_name]; +function flag_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'flag.list': + $output = '

' . t('This page lists all the flags that are currently defined on this system.') . '

'; + return $output; + case 'flag.add_page': + $output = '

' . t('Select the type of flag to create. An individual flag can only affect one type of object. This cannot be changed once the flag is created.') . '

'; + return $output; + case 'field_ui.overview_flagging': + // @todo: Doesn't make sense at the moment, implement when form + // functionality is available. + /* + // Get the existing link types that provide a flagging form. + $link_types = flag_get_link_types(); + $form_link_types = array(); + foreach (flag_get_link_types() as $link_type) { + if ($link_type['provides form']) { + $form_link_types[] = '' . $link_type['title'] . ''; + } } - } - } - // A menu loader has to return FALSE (not NULL) when no object is found. - return FALSE; -} -/** - * Menu title callback. - */ -function _flag_menu_title($flag) { - // The following conditional it to handle a D7 bug (@todo: link). - return $flag ? $flag->get_title() : ''; -} + // Get the flag for which we're managing fields. + $flag = menu_get_object('flag', FLAG_ADMIN_PATH_START + 1); + + // Common text. + $output = '

' . t('Flags can have fields added to them. For example, a "Spam" flag could have a Reason field where a user could type in why he believes the item flagged is spam. A "Bookmarks" flag could have a Folder field into which a user could arrange her bookmarks.') . '

'; + $output .= '

' . t('On this page you can add fields to flags, delete them, and otherwise manage them.') . '

'; + + // Three cases: + if ($flag->link_type == 'form') { + // Case 1: the current link type is the flagging form. Don't tell the + // user anything extra, all is fine. + } + elseif ($link_types[$flag->link_type]['provides form']) { + // Case 2: the current link type shows the form for creation of the + // flagging, but it not the flagging form. Tell the user they can't edit + // existing flagging fields. + $output .= t("Field values may be edited when flaggings are created because this flag's link type shows a form for the flagging. However, to edit field values on existing flaggings, you will need to set your flag to use the Flagging form link type. This is provided by the Flagging Form module.", array( + '!flagging-form-url' => 'http://drupal.org/project/flagging_form', + )); + if (!\Drupal::moduleHandler()->moduleExists('flagging_form')) { + $output .= ' ' + . t("You do not currently have this module enabled.") + . ''; + } + $output .= '

'; + } + else { + // Case 3: the current link type does not allow access to the flagging + // form. Tell the user they should change it. + $output .= '

' . t("To allow users to enter values for fields you will need to set your flag to use one of the following link types which allow users to access the flagging form: !link-types-list. (In case a form isn't used, the fields are assigned their default values.)", array( + '!form-link-type-url' => url('admin/structure/flags/manage/' . $flag->name, array('fragment' => 'edit-link-type')), + // The list of labels from link types. These are all defined in code + // in hook_flag_link_type_info() and therefore safe to output raw. + '!link-types-list' => implode(', ', $form_link_types), + )) . '

'; + $output .= '

' . t("Additionally, to edit field values on existing flaggings, you will need to set your flag to use the Flagging form link type. This is provided by the Flagging Form module.", array( + '!flagging-form-url' => 'http://drupal.org/project/flagging_form', + )); + if (!\Drupal::moduleHandler()->moduleExists('flagging_form')) { + $output .= ' ' + . t("You do not currently have this module enabled.") + . ''; + } + $output .= '

'; + } -/** - * Implements hook_help(). - */ -function flag_help($path, $arg) { - switch ($path) { - case FLAG_ADMIN_PATH: - $output = '

' . t('This page lists all the flags that are currently defined on this system.') . '

'; return $output; + */ } } /** - * Implements hook_init(). + * Implements hook_form_FORM_ID_alter(): user_admin_permissions(). + * + * Disable permission on the permissions form that don't make sense for + * anonymous users when Session API module is not enabled. */ -function flag_init() { - $path = drupal_get_path('module', 'flag'); - include_once $path .'/includes/flag.token.inc'; - if (module_exists('trigger')) { - include_once $path .'/includes/flag.actions.inc'; - } - if (module_exists('session_api')) { - // Set the anonymous user SID immediately, in case the user logs in. - flag_set_sid(); +function flag_form_user_admin_permissions_alter(&$form, FormStateInterface $form_state, $form_id) { + if (!\Drupal::moduleHandler()->moduleExists('session_api')) { + $flags = \Drupal::service('flag')->getFlags(); + // Disable flag and unflag permission checkboxes for anonymous users. + foreach ($flags as $flag_name => $flag) { + $form['checkboxes'][DRUPAL_ANONYMOUS_RID]["flag $flag_name"]['#disabled'] = TRUE; + $form['checkboxes'][DRUPAL_ANONYMOUS_RID]["unflag $flag_name"]['#disabled'] = TRUE; + } } } /** - * Implements hook_views_api(). + * Implements hook_entity_extra_field_info(). */ -function flag_views_api() { - return array( - 'api' => 2.0, - 'path' => drupal_get_path('module', 'flag') . '/includes', - ); -} +function flag_entity_extra_field_info() { + $extra = []; + $flag_service = \Drupal::service('flag'); + $flags = $flag_service->getFlags(); + foreach ($flags as $flag) { + // Skip flags that aren't on entities. + $flag_type_plugin = $flag->getFlagTypePlugin(); + if (!($flag_type_plugin instanceof \Drupal\flag\Plugin\Flag\EntityFlagType)) { + continue; + } -/** - * Implements hook_features_api(). - */ -function flag_features_api() { - return array( - 'flag' => array( - 'name' => t('Flag'), - 'feature_source' => TRUE, - 'default_hook' => 'flag_default_flags', - 'file' => drupal_get_path('module', 'flag') . '/includes/flag.features.inc', - ), - ); + foreach ($flag->types as $bundle_name) { + if ($flag_type_plugin->showOnForm()) { + $extra[$flag->entity_type][$bundle_name]['form']['flag'] = [ + 'label' => t('Flags'), + 'description' => t('Checkboxes for toggling flags'), + 'weight' => 10, + ]; + } + + if ($flag_type_plugin->showAsField()) { + $extra[$flag->entity_type][$bundle_name]['display']['flag_' . $flag->id()] = [ + // It would be nicer to use % as the placeholder, but the label is + // run through check_plain() by field_ui_display_overview_form() + // (arguably incorrectly; see http://drupal.org/node/1991292). + 'label' => t('Flag: @title', [ + '@title' => $flag->label, + ]), + 'description' => t('Individual flag link'), + 'weight' => 10, + ]; + } + } + } + + return $extra; } -/** - * Implements hook_permission(). - */ -function flag_permission() { - return array( - 'administer flags' => array( - 'title' => t('administer flags'), - 'description' => t('Create and edit site-wide flags.'), - ), - ); +function flag_node_links_alter(array &$links, NodeInterface $entity, array &$context) { + //@todo: Define this for handling the showOnLinks() flag mode. } /** - * Implements hook_link(). + * Implements hook_entity_view(). * - * This hook does not exist in Drupal 7, it is called from flag_node_view() and - * flag_comment_view() functions. + * Handles the 'show_in_links' and 'show_as_field' flag options. */ -function flag_link($type, $object = NULL, $teaser = FALSE) { - if (!isset($object) || !flag_fetch_definition($type)) { +function flag_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode, $langcode) { + // Don't show on previews. + if ($entity->isNew()) { return; } - global $user; - // Get all possible flags for this content-type. - $flags = flag_get_flags($type); + // @todo: Add caching here PLZ. - foreach ($flags as $flag) { - $content_id = $flag->get_content_id($object); + // Get all possible flags for this entity type. + $flag_service = \Drupal::service('flag'); + $flags = $flag_service->getFlags($entity->getEntityTypeID(), $entity->bundle()); - if (!$flag->uses_hook_link($teaser)) { - // Flag is not configured to show its link here. - continue; - } - if (!$flag->access($content_id) && (!$flag->is_flagged($content_id) || !$flag->access($content_id, 'flag'))) { - // User has no permission to use this flag or flag does not apply to this - // content. The link is not skipped if the user has "flag" access but - // not "unflag" access (this way the unflag denied message is shown). + foreach ($flags as $flag) { + // Do not display the flag if disabled. + if(!$flag->isEnabled()){ continue; } - // The flag links are actually fully rendered theme functions. - // The HTML attribute is set to TRUE to allow whatever the themer desires. - $links['flag-'. $flag->name] = array( - 'title' => $flag->theme($flag->is_flagged($content_id) ? 'unflag' : 'flag', $content_id), - 'html' => TRUE, - ); - } - - if (isset($links)) { - return $links; - } -} + $link_type_plugin = $flag->getLinkTypePlugin(); + $flag_type_plugin = $flag->getFlagTypePlugin(); -/** - * Implements hook_flag_link(). - * - * When Flag uses a link type provided by this module, it will call this - * implementation of hook_flag_link(). It returns a single link's attributes, - * using the same structure as hook_link(). Note that "title" is provided by - * the Flag configuration if not specified here. - * - * @param $flag - * The full flag object of for the flag link being generated. - * @param $action - * The action this link will perform. Either 'flag' or 'unflag'. - * @param $content_id - * The ID of the node, comment, user, or other object being flagged. - * @return - * An array defining properties of the link. - */ -function flag_flag_link($flag, $action, $content_id) { - $token = flag_get_token($content_id); - return array( - 'href' => 'flag/'. ($flag->link_type == 'confirm' ? 'confirm/' : '') ."$action/$flag->name/$content_id", - 'query' => drupal_get_destination() + ($flag->link_type == 'confirm' ? array() : array('token' => $token)), - ); -} - -/** - * Implements hook_flag_link_types(). - */ -function flag_flag_link_types() { - return array( - 'toggle' => array( - 'title' => t('JavaScript toggle'), - 'description' => t('An AJAX request will be made and degrades to type "Normal link" if JavaScript is not available.'), - 'uses standard js' => TRUE, - 'uses standard css' => TRUE, - ), - 'normal' => array( - 'title' => t('Normal link'), - 'description' => t('A normal non-JavaScript request will be made and the current page will be reloaded.'), - 'uses standard js' => FALSE, - 'uses standard css' => FALSE, - ), - 'confirm' => array( - 'title' => t('Confirmation form'), - 'description' => t('The user will be taken to a confirmation form on a separate page to confirm the flag.'), - 'options' => array( - 'flag_confirmation' => '', - 'unflag_confirmation' => '', - ), - 'uses standard js' => FALSE, - 'uses standard css' => FALSE, - ), - ); -} + // Only add cache key if flag link is displayed. + if ($flag_type_plugin->showAsField() && !$display->getComponent('flag_' . $flag->id())) { + continue; + } -/** - * Implements hook_field_extra_fields(). - */ -function flag_field_extra_fields() { - $extra = array(); - $flags = flag_get_flags('node'); - - // Filter out flags which need to be included on the node form. - foreach ($flags as $name => $flag) { - if (!$flag->show_on_form) { - unset($flags[$name]); + $action = 'flag'; + if ($flag->isFlagged($entity)) { + $action = 'unflag'; } - } - if (!empty($flags)) { - foreach (node_type_get_types() as $type) { - $extra['node'][$type->type]['flag'] = array( - 'label' => t('Flags'), - 'description' => t('Flags fieldset.'), - 'weight' => 0 - ); + // If the user does not have permission, go to the next foreach loop and + // don't display this flag. + if(!$flag->hasActionAccess($action)){ + continue; } + + $link = $link_type_plugin->renderLink($action, $flag, $entity); + $build['flag_' . $flag->id()] = $link; } - return $extra; } /** - * Implements hook_form_alter(). + * Implements hook_entity_build_defaults_alter(). */ -function flag_form_alter(&$form, &$form_state, $form_id) { - global $user; +function flag_entity_build_defaults_alter(array &$build, EntityInterface $entity, $view_mode = 'full', $langcode = NULL) { + // Add the flag ID combined with the action to the cache key if render + // caching is enabled. + if (isset($build['#cache']) && isset($build['#cache']['keys'])) { - if ($form_id == 'node_type_form') { - $flags = flag_get_flags('node', $form['#node_type']->type, $user); - foreach ($flags as $flag) { - if ($flag->show_on_form) { - $var = 'flag_'. $flag->name .'_default'; - $form['workflow']['flag'][$var] = array( - '#type' => 'checkbox', - '#title' => $flag->get_label('flag_short', $form['#node_type']->type), - '#default_value' => variable_get($var .'_'. $form['#node_type']->type, 0), - '#return_value' => 1, - ); - } - } + // Get all possible flags for this entity type. + $flag_service = \Drupal::service('flag'); + $flags = $flag_service->getFlags($entity->getEntityTypeID(), + $entity->bundle()); - if (isset($form['workflow']['flag'])) { - $form['workflow']['flag'] += array( - '#type' => 'item', - '#title' => t('Default flags'), - '#description' => t('Above are the flags you elected to show on the node editing form. You may specify their initial state here.', array('@flag-url' => url(FLAG_ADMIN_PATH))), - // Make the spacing a bit more compact: - '#prefix' => '
', - '#suffix' => '
', - ); - } - } - elseif (isset($form['type']) && isset($form['#node']) && ($form_id == $form['type']['#value'] .'_node_form')) { - $nid = !empty($form['nid']['#value']) ? $form['nid']['#value'] : NULL; - $flags = flag_get_flags('node', $form['type']['#value'], $user); - - // Filter out flags which need to be included on the node form. - $flags_in_form = 0; - $flags_visible = 0; - foreach ($flags as $name => $flag) { - if (!$flag->show_on_form) { - continue; - } + // Get the corresponding display settings. + $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode); - if (isset($form['#node']->flag[$flag->name])) { - $flag_status = $form['#node']->flag[$flag->name]; - } - else { - $flag_status_default = variable_get('flag_' . $flag->name . '_default_' . $form['type']['#value'], 0); - $flag_status = $nid ? $flag->is_flagged($nid) : $flag_status_default; - } + foreach ($flags as $flag) { + $flag_type_plugin = $flag->getFlagTypePlugin(); - // If the flag is not global and the user doesn't have access, skip it. - // Global flags have their value set even if the user doesn't have access - // to it, similar to the way "published" and "promote" keep the default - // values even if the user doesn't have "administer nodes" permission. - $access = $flag->access($nid, $flag_status ? 'unflag' : 'flag', $user); - if (!$access && !$flag->global) { + // Only add cache key if flag link is displayed. + if ($flag_type_plugin->showAsField() && !$display->getComponent('flag_' . $flag->id())) { continue; } - // Add the flag checkbox if displaying on the form. - $form['flag'][$flag->name] = array( - '#type' => 'checkbox', - '#title' => $flag->get_label('flag_short', $nid ? $nid : $form['type']['#value']), - '#description' => $flag->get_label('flag_long', $nid ? $nid : $form['type']['#value']), - '#default_value' => $flag_status, - '#return_value' => 1, - '#attributes' => array('title' => $flag->get_title()), // Used by our drupalSetSummary(). - ); - // If the user does not have access to the flag, set as a value. - if (!$access) { - $form['flag'][$flag->name]['#type'] = 'value'; - $form['flag'][$flag->name]['#value'] = $flag_status; + $action = 'flag'; + if ($flag->isFlagged($entity)) { + $action = 'unflag'; } - else { - $flags_visible++; - } - $flags_in_form++; - } - if ($flags_in_form) { - $form['flag'] += array( - '#weight' => module_exists('content') ? content_extra_field_weight($form['#node']->type, 'flags') : 1, - '#tree' => TRUE, - ); - } - if ($flags_visible) { - $form['flag'] += array( - '#type' => 'fieldset', - '#title' => t('Flags'), - '#collapsible' => TRUE, - // Vertical tabs support: - '#group' => 'additional_settings', - '#attributes' => array('class' => array('flag-fieldset')), - '#attached' => array( - 'js' => array( - 'vertical-tabs' => drupal_get_path('module', 'flag') . '/theme/flag-admin.js', - ), - ), - ); - } - - } -} -/** - * Implements hook_comment_view(). - */ -function flag_comment_view($comment) { - $links = flag_link('comment', $comment); - $comment->content['links']['flag'] = array( - '#theme' => 'links', - '#links' => $links, - '#attributes' => array('class' => array('links', 'inline')), - ); -} - -/** - * Implements hook_node_view(). - */ -function flag_node_view($node, $view_mode) { - $links = flag_link('node', $node, $view_mode == 'teaser'); - $node->content['links']['flag'] = array( - '#theme' => 'links__node__flag', - '#links' => $links, - '#attributes' => array('class' => array('links', 'inline')), - ); -} - -/** - * Implements hook_node_insert(). - */ -function flag_node_insert($node) { - flag_node_save($node); -} - -/** - * Implements hook_node_update(). - */ -function flag_node_update($node) { - flag_node_save($node); -} - -/** - * Shared saving routine between flag_node_insert() and flag_node_update(). - */ -function flag_node_save($node) { - // Response to the flag checkboxes added to the form in flag_form_alter(). - $remembered = FALSE; - $account = user_load($node->uid); - if (isset($node->flag)) { - foreach ($node->flag as $name => $state) { - $flag = flag_get_flag($name); - // Flagging may trigger actions. We want actions to get the current - // node, not a stale database-loaded one: - if (!$remembered) { - $flag->remember_content($node->nid, $node); - // Actions may modify a node, and we don't want to overwrite this - // modification: - $remembered = TRUE; - } - flag($state ? 'flag' : 'unflag', $name, $node->nid, $account, TRUE); + $build['#cache']['keys'][] = $flag->id . '-' . $action; } } + return $build; } /** - * Implements hook_node_delete(). + * Implements hook_entity_delete(). */ -function flag_node_delete($node) { - foreach (flag_get_flags('node') as $flag) { - // If the flag is being tracked by translation set and the node is part - // of a translation set, don't delete the flagging record. - // Instead, data will be updated in the 'translation_change' op, below. - if (!$flag->i18n || empty($node->tnid)) { - db_delete('flag_content') - ->condition('fid', $flag->fid) - ->condition('content_id', $node->nid); - db_delete('flag_counts') - ->condition('fid', $flag->fid) - ->condition('content_id', $node->nid); - } +function flag_entity_delete(EntityInterface $entity) { + // Node and user flags handle things through the entity type delete hooks. + // @todo: make this configurable in the flag type definition? + if ($entity->getEntityTypeId() == 'node' || $entity->getEntityTypeId() == 'user') { + return; } + + // @todo Actually delete the flaggings and clear associated flag counts. } /** - * Implements hook_translation_change(). + * Implements hook_user_cancel(). */ -function flag_node_translation_change($node) { - if (isset($node->translation_change)) { - // If there is only one node remaining, track by nid rather than tnid. - // Otherwise, use the new tnid. - $content_id = $node->translation_change['new_tnid'] == 0 ? $node->translation_change['remaining_nid'] : $node->translation_change['new_tnid']; - foreach (flag_get_flags('node') as $flag) { - if ($flag->i18n) { - db_update('flag_content')->fields(array('content_id' => $content_id)) - ->condition('fid', $flag->fid) - ->condition('content_id', $node->translation_change['old_tnid']) - ->execute(); - db_update('flag_counts')->fields(array('content_id' => $content_id)) - ->condition('fid', $flag->fid) - ->condition('content_id', $node->translation_change['old_tnid']) - ->execute(); - } - } - } +function flag_user_cancel($edit, $account, $method) { + flag_user_account_removal($account); } /** - * Implements hook_user_login(). + * Implements hook_user_delete(). */ -function flag_user_login(&$edit, &$account) { - // Migrate anonymous flags to this user's account. - if (module_exists('session_api') && ($sid = flag_get_sid(0))) { - // The @ symbol suppresses errors if the user flags a piece of content - // they have already flagged as a logged-in user. - @db_update('flag_content') - ->fields(array( - 'uid' => $account->uid, - 'sid' => 0, - )) - ->condition('uid', 0) - ->condition('sid', $sid) - ->execute(); - - // Delete any remaining flags this user had as an anonymous user. - db_delete('flag_content') - ->condition('uid', 0) - ->condition('sid', flag_get_sid(0)) - ->execute(); - - // Clean up anonymous cookies. - FlagCookieStorage::drop(); - } +function flag_user_delete(UserInterface $account) { + flag_user_account_removal($account); } /** - * Implements hook_user_cancel(). + * Shared helper for user account cancellation or deletion. */ -function flag_user_cancel($edit, $account, $method) { +function flag_user_account_removal(UserInterface $account) { // Remove flags by this user. - $query = db_select('flag_content', 'fc'); - $query->leftJoin('flag_counts', 'c', 'fc.content_id = c.content_id AND fc.content_type = c.content_type'); + $query = db_select('flagging', 'fc'); + $query->leftJoin('flag_counts', 'c', 'fc.entity_id = c.entity_id AND fc.entity_type = c.entity_type'); $result = $query - ->fields('fc', array('fid', 'content_id')) - ->fields('c', array('count')) - ->condition('fc.uid', $account->uid) + ->fields('fc', ['fid', 'entity_id']) + ->fields('c', ['count']) + ->condition('fc.uid', $account->id()) ->execute(); foreach ($result as $flag_data) { - $flag_data->count--; - db_update('flag_counts') - ->fields(array( - 'count' => $flag_data->count, - )) - ->condition('fid', $flag_data->fid) - ->condition('content_id', $flag_data->content_id) - ->execute(); - } - db_delete('flag_content') - ->condition('uid', $account->uid) - ->execute(); -} - -/** - * Implements hook_user_view(). - */ -function flag_user_view($account, $view_mode) { - $flags = flag_get_flags('user'); - $flag_items = array(); - foreach ($flags as $flag) { - if (!$flag->access($account->uid)) { - // User has no permission to use this flag. - continue; - } - if (!$flag->uses_hook_link(array())){ - // Flag not set to appear on profile. - continue; - } - $flag_items[$flag->name] = array( - '#type' => 'user_profile_item', - '#title' => $flag->get_title($account->uid), - '#markup' => $flag->theme($flag->is_flagged($account->uid) ? 'unflag' : 'flag', $account->uid), - '#attributes' => array('class' => array('flag-profile-' . $flag->name)), - ); - } - if (!empty($flag_items)) { - $account->content['flags'] = $flag_items; - $account->content['flags'] += array( - '#type' => 'user_profile_category', - '#title' => t('Actions'), - '#attributes' => array('class' => array('flag-profile')), - ); - } -} - -/** - * Implements hook_session_api_cleanup(). - * - * Clear out anonymous user flaggings during Session API cleanup. - */ -function flag_session_api_cleanup($arg = 'run') { - // Session API 1.1 version: - if ($arg == 'run') { - $query = db_select('flag_content', 'fc'); - $query->leftJoin('session_api', 's', 'fc.sid = s.sid'); - $result = $query - ->fields('fc', array('sid')) - ->condition('fc.sid', 0, '<>') - ->isNull('s.sid') - ->execute(); - foreach ($result as $row) { - db_delete('flag_content') - ->condition('sid', $row->sid) + // Only decrement the flag count table if it's greater than 1. + if ($flag_data->count > 0) { + $flag_data->count--; + db_update('flag_counts') + ->fields([ + 'count' => $flag_data->count, + ]) + ->condition('fid', $flag_data->fid) + ->condition('entity_id', $flag_data->entity_id) ->execute(); } - } - // Session API 1.2+ version. - elseif (is_array($arg)) { - $outdated_sids = $arg; - db_delete('flag_content')->condition('sid', $outdated_sids, 'IN')->execute(); - } -} - -/** - * Implements hook_node_type(). - */ -function flag_node_type_delete($info) { - // Remove entry from flaggable content types. - db_delete('flag_types')->condition('type', $info->type)->execute(); -} - -/** - * Menu callback for (un)flagging a node. - * - * Used both for the regular callback as well as the JS version. - * - * @param $action - * Either 'flag' or 'unflag'. - */ -function flag_page($action, $flag, $content_id) { - global $user; - - // Shorten up the variables that affect the behavior of this page. - $js = isset($_REQUEST['js']); - $token = $_REQUEST['token']; - - // Specifically $_GET to avoid getting the $_COOKIE variable by the same key. - $has_js = isset($_GET['has_js']); - - // Check the flag token, then perform the flagging. - if (!flag_check_token($token, $content_id)) { - $error = t('Bad token. You seem to have followed an invalid link.'); - } - elseif ($user->uid == 0 && !$has_js) { - $error = t('You must have JavaScript and cookies enabled in your browser to flag content.'); - } - else { - $result = $flag->flag($action, $content_id); - if (!$result) { - $error = t('You are not allowed to flag, or unflag, this content.'); - } - } - - // If an error was received, set a message and exit. - if (isset($error)) { - if ($js) { - drupal_add_http_header('Content-Type', 'text/javascript; charset=utf-8'); - print drupal_json_encode(array( - 'status' => FALSE, - 'errorMessage' => $error, - )); - exit; - } - else { - drupal_set_message($error); - drupal_access_denied(); - return; + elseif ($flag_data->count == 0) { + db_delete('flag_counts') + ->condition('fid', $flag_data->fid) + ->condition('entity_id', $flag_data->entity_id) + ->execute(); } } + db_delete('flagging') + ->condition('uid', $account->id()) + ->execute(); - // If successful, return data according to the request type. - if ($js) { - drupal_add_http_header('Content-Type', 'text/javascript; charset=utf-8'); - $flag->link_type = 'toggle'; - print drupal_json_encode(array( - 'status' => TRUE, - 'newLink' => $flag->theme($flag->is_flagged($content_id) ? 'unflag' : 'flag', $content_id, TRUE), - // Further information for the benefit of custom JavaScript event handlers: - 'contentId' => $content_id, - 'contentType' => $flag->content_type, - 'flagName' => $flag->name, - 'flagStatus' => $flag->is_flagged($content_id) ? 'flagged' : 'unflagged', - )); - exit; - } - else { - drupal_set_message($flag->get_label($action . '_message', $content_id)); - drupal_goto(); - } -} - -/** - * Form for confirming the (un)flagging of a piece of content. - * - * @param $action - * Either 'flag' or 'unflag'. - */ -function flag_confirm($form, &$form_state, $action, $flag, $content_id) { - $form['action'] = array( - '#type' => 'value', - '#value' => $action, - ); - $form['flag_name'] = array( - '#type' => 'value', - '#value' => $flag->name, - ); - $form['content_id'] = array( - '#type' => 'value', - '#value' => $content_id, - ); - - $question = $flag->get_label($action .'_confirmation', $content_id); - $path = isset($_GET['destination']) ? $_GET['destination'] : ''; - $yes = $flag->get_label($action .'_short', $content_id); - - return confirm_form($form, $question, $path, '', $yes); -} - -function flag_confirm_submit(&$form, &$form_state) { - $action = $form_state['values']['action']; - $flag_name = $form_state['values']['flag_name']; - $content_id = $form_state['values']['content_id']; - - $result = flag($action, $flag_name, $content_id); - if (!$result) { - drupal_set_message(t('You are not allowed to flag, or unflag, this content.')); - } - else { - $flag = flag_get_flag($flag_name); - drupal_set_message($flag->get_label($action . '_message', $content_id)); - } -} - -/** - * Flags or unflags an item. - * - * @param $account - * The user on whose behalf to flag. Leave empty for the current user. - * @return - * FALSE if some error occured (e.g., user has no permission, flag isn't - * applicable to the item, etc.), TRUE otherwise. - */ -function flag($action, $flag_name, $content_id, $account = NULL) { - if (!($flag = flag_get_flag($flag_name))) { - // Flag does not exist. - return FALSE; - } - return $flag->flag($action, $content_id, $account); + // Remove flags that have been done to this user. + //_flag_entity_delete('user', $account->id()); } /** - * Implements hook_flag(). Trigger actions if any are available. + * Implements hook_flag_access(). */ -function flag_flag($action, $flag, $content_id, $account) { - if (module_exists('trigger')) { - $context['hook'] = 'flag'; - $context['account'] = $account; - $context['flag'] = $flag; - $context['op'] = $action; - // We add to the $context all the objects we know about: - $context = array_merge($flag->get_relevant_action_objects($content_id), $context); - // The primary object the actions work on. - $object = $flag->fetch_content($content_id); - - // Generic "all flags" actions. - foreach (trigger_get_assigned_actions('flag_' . $action) as $aid => $action_info) { - // The 'if ($aid)' is a safeguard against http://drupal.org/node/271460#comment-886564 - if ($aid) { - actions_do($aid, $object, $context); - } - } - // Actions specifically for this flag. - foreach (trigger_get_assigned_actions('flag_' . $action . '_' . $flag->name) as $aid => $action_info) { - if ($aid) { - actions_do($aid, $object, $context); - } - } +function flag_flag_access($flag, $entity_id, $action, $account) { + // Do nothing if there is no restriction by authorship. + if (empty($flag->access_author)) { + return; } - if (module_exists('rules')) { - $event_name = ($action == 'flag' ? 'flag_flagged_' : 'flag_unflagged_') . $flag->name; - // We only support flags on entities. - if (entity_get_info($flag->content_type)) { - $variables = array('flag' => $flag, 'flagged_' . $flag->content_type => $content_id, 'flagging_user' => $account); - rules_invoke_event_by_args($event_name, $variables); - } - } -} - -/** - * Implements hook_flag_access(). - */ -function flag_flag_access($flag, $content_id, $action, $account) { // Restrict access by authorship. It's important that TRUE is never returned // here, otherwise we'd grant permission even if other modules denied access. - if ($flag->content_type == 'node') { + if ($flag->entity_type == 'node') { // For non-existent nodes (such as on the node add form), assume that the // current user is creating the content. - if (empty($content_id) || !($node = node_load($content_id))) { - return $flag->access_author == 'others' ? FALSE : NULL; + if (empty($entity_id) || !($node = $flag->fetch_entity($entity_id))) { + return AccessResult::allowedIf($flag->access_author == 'others')->cacheUntilEntityChanges($flag); } if ($flag->access_author == 'own' && $node->uid != $account->uid) { - return FALSE; + return AccessResult::forbidden()->cacheUntilEntityChanges($flag); } elseif ($flag->access_author == 'others' && $node->uid == $account->uid) { - return FALSE; + return AccessResult::forbidden()->cacheUntilEntityChanges($flag); } } // Restrict access by comment authorship. - if ($flag->content_type == 'comment') { + if ($flag->entity_type == 'comment') { // For non-existent comments (such as on the comment add form), assume that // the current user is creating the content. - if (empty($content_id) || !($comment = $flag->fetch_content($content_id))) { - return $flag->access_author == 'comment_others' ? FALSE : NULL; + if (empty($entity_id) || !($comment = $flag->fetch_entity($entity_id))) { + return $flag->access_author == 'comment_others' ? AccessResult::forbidden()->cacheUntilEntityChanges($flag) : NULL; } - $node = node_load($comment->nid); + $node = \Drupal::entityManager()->getStorage('node')->load($comment->nid); if ($flag->access_author == 'node_own' && $node->uid != $account->uid) { - return FALSE; + return AccessResult::forbidden()->cacheUntilEntityChanges($flag); } elseif ($flag->access_author == 'node_others' && $node->uid == $account->uid) { - return FALSE; + return AccessResult::forbidden()->cacheUntilEntityChanges($flag); } elseif ($flag->access_author == 'comment_own' && $comment->uid != $account->uid) { - return FALSE; + return AccessResult::forbidden()->cacheUntilEntityChanges($flag); } elseif ($flag->access_author == 'comment_others' && $comment->uid == $account->uid) { - return FALSE; + return AccessResult::forbidden()->cacheUntilEntityChanges($flag); } } } @@ -893,17 +360,22 @@ function flag_flag_access($flag, $content_id, $action, $account) { /** * Implements hook_flag_access_multiple(). */ -function flag_flag_access_multiple($flag, $content_ids, $account) { +function flag_flag_access_multiple($flag, $entity_ids, $account) { $access = array(); - if ($flag->content_type == 'node') { + // Do nothing if there is no restriction by authorship. + if (empty($flag->access_author)) { + return $access; + } + + if ($flag->entity_type == 'node') { // Restrict access by authorship. This is similar to flag_flag_access() // above, but returns an array of 'nid' => $access values. Similarly, we // should never return TRUE in any of these access values, only FALSE if we // want to deny access, or use the current access value provided by Flag. $result = db_select('node', 'n') ->fields('n', array('nid', 'uid')) - ->condition('nid', $content_ids, 'IN') + ->condition('nid', array_keys($entity_ids), 'IN') ->condition('type', $flag->types, 'IN') ->execute(); foreach ($result as $row) { @@ -916,17 +388,17 @@ function flag_flag_access_multiple($flag, $content_ids, $account) { } } - if ($flag->content_type == 'comment') { + if ($flag->entity_type == 'comment') { // Restrict access by comment ownership. $query = db_select('comment', 'c'); $query->leftJoin('node', 'n', 'c.nid = n.nid'); - $result = $query - ->fields('c', array('cid', 'nid')) - ->addField('c', 'uid', 'comment_uid') - ->addField('n', 'uid', 'node_uid') - ->condition('c.cid', $content_ids, 'IN') - ->execute(); - while ($row = db_fetch_object($result)) { + $query + ->fields('c', array('cid', 'nid', 'uid')) + ->condition('c.cid', $entity_ids, 'IN'); + $query->addField('c', 'uid', 'comment_uid'); + $result = $query->execute(); + + foreach ($result as $row) { if ($flag->access_author == 'node_own') { $access[$row->cid] = $row->node_uid != $account->uid ? FALSE : NULL; } @@ -946,436 +418,110 @@ function flag_flag_access_multiple($flag, $content_ids, $account) { return $access; } -/** - * Trim a flag to a certain size. - * - * @param $fid - * The flag object. - * @param $account - * The user object on behalf the trimming will occur. - * @param $cutoff_size - * The number of flaggings allowed. Any flaggings beyond that will be trimmed. - */ -function flag_trim_flag($flag, $account, $cutoff_size) { - $result = db_select('flag_content', 'fc') - ->fields('fc') - ->condition('fid', $flag->fid) - ->condition(db_or()->condition('uid', $account->uid)->condition('uid', 0)) - ->orderBy('timestamp', 'DESC') - ->execute(); - $i = 1; - foreach ($result as $row) { - if ($i++ > $cutoff_size) { - flag('unflag', $flag->name, $row->content_id, $account); - } - } -} +// --------------------------------------------------------------------------- +// Non-Views public API /** - * Remove all flagged content from a flag. + * Get the count of flags for a certain entity. * * @param $flag - * The flag object. - * @param $content_id - * Optional. The content ID on which all flaggings will be removed. If left - * empty, this will remove all of this flag's content. - */ -function flag_reset_flag($flag, $content_id = NULL) { - $query = db_select('flag_content', 'fc') - ->fields('fc') - ->condition('fid', $flag->fid); - - if ($content_id) { - $query->condition('content_id', $content_id); - } - - $result = $query->execute()->fetchAllAssoc('fcid', PDO::FETCH_ASSOC);; - $rows = array(); - foreach ($result as $row) { - $rows[] = $row; - } - module_invoke_all('flag_reset', $flag, $content_id, $rows); - - $query = db_delete('flag_content')->condition('fid' , $flag->fid); - if ($content_id) { - $query->condition('content_id', $content_id); - } - return $query->execute(); -} - -/** - * Implements hook_node_operations(). + * The flag. + * @param $entity_type + * The entity type (usually 'node'). * - * Add additional options on the admin/build/node page. - */ -function flag_node_operations() { - global $user; - - $flags = flag_get_flags('node', NULL, $user); - $operations = array(); - - foreach ($flags as $flag) { - $operations['flag_' . $flag->name] = array( - 'label' => $flag->get_label('flag_short'), - 'callback' => 'flag_nodes', - 'callback arguments' => array('flag', $flag->name), - 'behavior' => array(), - ); - $operations['unflag_' . $flag->name] = array( - 'label' => $flag->get_label('unflag_short'), - 'callback' => 'flag_nodes', - 'callback arguments' => array('unflag', $flag->name), - 'behavior' => array(), - ); - } - return $operations; -} - -/** - * Callback function for hook_node_operations(). - */ -function flag_nodes($nodes, $action, $flag_name) { - $performed = FALSE; - foreach ($nodes as $nid) { - $performed |= flag($action, $flag_name, $nid); - } - if ($performed) { - drupal_set_message(t('The update has been performed.')); - } -} - -/** - * Implements hook_user_operations(). + * @return + * The flag count with the flag name and entity type as the array key. + * + * @deprecated In Drupal 8. + * @todo Move flag_get_entity_flag_counts() into FlagCountService. */ -function flag_user_operations() { - global $user; - - $flags = flag_get_flags('user', NULL, $user); - $operations = array(); - - foreach ($flags as $flag) { - $operations['flag_' . $flag->name] = array( - 'label' => $flag->get_label('flag_short'), - 'callback' => 'flag_users', - 'callback arguments' => array('flag', $flag->name), - ); - $operations['unflag_' . $flag->name] = array( - 'label' => $flag->get_label('unflag_short'), - 'callback' => 'flag_users', - 'callback arguments' => array('unflag', $flag->name), - ); - } - return $operations; -} -/** -* Callback function for hook_user_operations(). -*/ -function flag_users($users, $action, $flag_name) { - foreach ($users as $uid) { - flag($action, $flag_name, $uid); - } -} +function flag_get_entity_flag_counts($flag, $entity_type) { + $counts = &drupal_static(__FUNCTION__); -/** - * Implements hook_mail(). - */ -function flag_mail($key, &$message, $params) { - switch ($key) { - case 'over_threshold': - $message['subject'] = $params['subject']; - $message['body'] = $params['body']; - break; + // We check to see if the flag count is already in the cache, + // if it's not, run the query. + if (!isset($counts[$flag->name][$entity_type])) { + $counts[$flag->name][$entity_type] = []; + $result = db_select('flagging', 'f') + ->fields('f', ['fid']) + ->condition('fid', $flag->fid) + ->condition('entity_type', $entity_type) + ->countQuery() + ->execute() + ->fetchField(); + $counts[$flag->name][$entity_type] = $result; } -} -/** - * Implements hook_service(). - */ -function flag_service() { - $items = array(); - - $items[] = array( - '#method' => 'flag.flag', - '#callback' => 'flag_service_flag', - '#access callback' => 'flag_service_flag_access', - '#file' => array( - 'file' => 'inc', - 'module' => 'flag', - 'file name' => 'includes/flag.services', - ), - '#args' => array( - array( - '#name' => 'flag_name', - '#type' => 'string', - '#description' => t('The name of the flag.'), - ), - array( - '#name' => 'content_id', - '#type' => 'int', - '#description' => t('The content ID.'), - ), - array( - '#name' => 'uid', - '#type' => 'int', - '#description' => t('The user ID for which to flag.'), - '#optional' => TRUE, - ), - array( - '#name' => 'action', - '#type' => 'string', - '#description' => t('Optional; The action to perform, default is "flag". Should be "flag" or "unflag".'), - '#optional' => TRUE, - ), - array( - '#name' => 'skip_permission_check', - '#type' => 'boolean', - '#description' => t('Optional; Flag the content even if the user does not have permission to do so. FALSE by default'), - '#optional' => TRUE, - ), - ), - '#return' => 'boolean', - '#help' => t('Flags (or unflags) a content.') - ); - - $items[] = array( - '#method' => 'flag.is_flagged', - '#callback' => 'flag_service_is_flagged', - '#access callback' => 'flag_service_flag_access', - '#file' => array( - 'file' => 'inc', - 'module' => 'flag', - 'file name' => 'includes/flag.services', - ), - '#args' => array( - array( - '#name' => 'flag_name', - '#type' => 'string', - '#description' => t('The name of the flag.'), - ), - array( - '#name' => 'content_id', - '#type' => 'int', - '#description' => t('The content ID.'), - ), - array( - '#name' => 'uid', - '#type' => 'int', - '#description' => t('The user ID that might have flagged the content.'), - '#optional' => TRUE, - ), - ), - '#return' => 'boolean', - '#help' => t('Check if a content was flagged by a user.') - ); - - return $items; + return $counts[$flag->name][$entity_type]; } /** - * Implements hook_theme(). - */ -function flag_theme() { - $path = drupal_get_path('module', 'flag') .'/theme'; - - return array( - 'flag' => array( - 'variables' => array('flag' => NULL, 'action' => NULL, 'content_id' => NULL, 'after_flagging' => FALSE), - 'template' => 'flag', - 'pattern' => 'flag__', - 'path' => $path, - ), - 'flag_tokens_browser' => array( - 'variables' => array('types' => array('all'), 'global_types' => TRUE), - 'file' => 'includes/flag.token.inc', - ), - 'flag_admin_page' => array( - 'variables' => array('flags' => NULL, 'default_flags' => NULL), - 'file' => 'includes/flag.admin.inc', - ), - 'flag_form_roles' => array( - 'render element' => 'element', - 'file' => 'includes/flag.admin.inc', - ), - 'flag_rules_radios' => array( - 'render element' => 'element', - 'file' => 'flag.rules.inc', - ), - ); -} - -/** - * A preprocess function for our theme('flag'). It generates the - * variables needed there. + * Get the user's flag count. * - * The $variables array initially contains the following arguments: - * - $flag - * - $action - * - $content_id - * - $after_flagging + * @param $flag + * The flag. + * @param $user + * The user object. * - * See 'flag.tpl.php' for their documentation. - */ -function template_preprocess_flag(&$variables) { - global $user; - static $initialized = array(); - - // Some typing shotcuts: - $flag =& $variables['flag']; - $action = $variables['action']; - $content_id = $variables['content_id']; - $flag_css_name = str_replace('_', '-', $flag->name); - - // Generate the link URL. - $link_type = $flag->get_link_type(); - $link = module_invoke($link_type['module'], 'flag_link', $flag, $action, $content_id); - if (isset($link['title']) && empty($link['html'])) { - $link['title'] = check_plain($link['title']); - } - - // Replace the link with the access denied text if unable to flag. - if ($action == 'unflag' && !$flag->access($content_id, 'unflag')) { - $link['title'] = $flag->get_label('unflag_denied_text', $content_id); - unset($link['href']); - } - - // Anonymous users always need the JavaScript to maintain their flag state. - if ($user->uid == 0) { - $link_type['uses standard js'] = TRUE; - } - - // Load the JavaScript/CSS, if the link type requires it. - if (!isset($initialized[$link_type['name']])) { - if ($link_type['uses standard css']) { - drupal_add_css(drupal_get_path('module', 'flag') . '/theme/flag.css'); - } - if ($link_type['uses standard js']) { - drupal_add_js(drupal_get_path('module', 'flag') . '/theme/flag.js'); - } - $initialized[$link_type['name']] = TRUE; - } - - $variables['link_href'] = isset($link['href']) ? check_url(url($link['href'], $link)) : FALSE; - $variables['link_text'] = isset($link['title']) ? $link['title'] : $flag->get_label($action . '_short', $content_id); - $variables['link_title'] = isset($link['attributes']['title']) ? check_plain($link['attributes']['title']) : check_plain(strip_tags($flag->get_label($action . '_long', $content_id))); - $variables['status'] = ($action == 'flag' ? 'unflagged' : 'flagged'); - - $variables['flag_wrapper_classes_array'] = array(); - $variables['flag_wrapper_classes_array'][] = 'flag-wrapper'; - $variables['flag_wrapper_classes_array'][] = 'flag-' . $flag_css_name; - $variables['flag_wrapper_classes_array'][] = 'flag-' . $flag_css_name . '-' . $content_id; - $variables['flag_wrapper_classes'] = implode(' ', $variables['flag_wrapper_classes_array']); - - $variables['flag_classes_array'] = array(); - $variables['flag_classes_array'][] = 'flag'; - $variables['flag_classes_array'][] = $variables['action'] .'-action'; - $variables['flag_classes_array'][] = 'flag-link-'. $flag->link_type; - if (isset($link['attributes']['class'])) { - $variables['flag_classes_array'][] = $link['attributes']['class']; - } - if ($variables['after_flagging']) { - $inverse_action = ($action == 'flag' ? 'unflag' : 'flag'); - $variables['message_text'] = $flag->get_label($inverse_action . '_message', $content_id); - $variables['flag_classes_array'][] = $variables['status']; - } - $variables['flag_classes'] = implode(' ', $variables['flag_classes_array']); - - // Backward compatibility: The following section preserves some deprecated - // variables either to make old flag.tpl.php files continue to work, or to - // prevent PHP from generating E_NOTICEs there. @todo: Remove these sometime. - $variables['setup'] = FALSE; - $variables['last_action'] = $variables['status']; -} - -/** - * Return an array of flag names keyed by fid. + * @return + * The flag count with the flag name and the uid as the array key. + * + * @deprecated In Drupal 8. + * @todo Move flag_get_user_flag_counts() to FlagCountsService. */ -function _flag_get_flag_names() { - $flags = flag_get_flags(); - $flag_names = array(); - foreach ($flags as $flag) { - $flag_names[$flag->fid] = $flag->name; - } - return $flag_names; -} +function flag_get_user_flag_counts($flag, $user) { + $counts = &drupal_static(__FUNCTION__); -/** - * Return an array of flag link types suitable for a select list or radios. - */ -function _flag_link_type_options() { - $options = array(); - $types = flag_get_link_types(); - foreach ($types as $type_name => $type) { - $options[$type_name] = $type['title']; + // We check to see if the flag count is already in the cache, + // if it's not, run the query. + if (!isset($counts[$flag->name][$user->uid])) { + $counts[$flag->name][$user->uid] = []; + $result = db_select('flagging', 'f') + ->fields('f', ['fid']) + ->condition('fid', $flag->fid) + ->condition('uid', $user->uid) + ->countQuery() + ->execute() + ->fetchField(); + $counts[$flag->name][$user->uid] = $result; } - return $options; -} -/** - * Return an array of flag link type descriptions. - */ -function _flag_link_type_descriptions() { - $options = array(); - $types = flag_get_link_types(); - foreach ($types as $type_name => $type) { - $options[$type_name] = $type['description']; - } - return $options; -} - -/** - * Return an array of flag link fields that are dependent on a link type. - */ -function _flag_link_type_fields() { - $options = array(); - $types = flag_get_link_types(); - foreach ($types as $type_name => $type) { - $options[$type_name] = array_keys($type['options']); - } - return $options; + return $counts[$flag->name][$user->uid]; } -// --------------------------------------------------------------------------- -// Non-Views public API - /** * Get flag counts for all flags on a node. * - * @param $content_type - * The content type (usually 'node'). - * @param $content_id - * The content ID (usually the node ID). - * @param $reset - * Reset the internal cache and execute the SQL query another time. + * @param $entity_type + * The entity type (usually 'node'). + * @param $entity_id + * The entity ID (usually the node ID). + * + * @return + * The flag count with the entity type and id as array keys. * - * @return $flags - * An array of the structure [name] => [number of flags]. + * @deprecated In Drupal 8 + * @todo Move flag_get_counts() to FlagCountService. */ -function flag_get_counts($content_type, $content_id, $reset = FALSE) { - static $counts; +function flag_get_counts($entity_type, $entity_id) { + $counts = &drupal_static(__FUNCTION__); - if ($reset) { - $counts = array(); - if (!isset($content_type)) { - return; - } - } - - if (!isset($counts[$content_type][$content_id])) { - $counts[$content_type][$content_id] = array(); - $query = db_select('flags', 'f'); + if (!isset($counts[$entity_type][$entity_id])) { + $counts[$entity_type][$entity_id] = []; + $query = db_select('flag', 'f'); $query->leftJoin('flag_counts', 'fc', 'f.fid = fc.fid'); $result = $query - ->fields('f', array('name')) - ->fields('fc', array('count')) - ->condition('fc.content_type', $content_type) - ->condition('fc.content_id', $content_id) + ->fields('f', ['name']) + ->fields('fc', ['count']) + ->condition('fc.entity_type', $entity_type) + ->condition('fc.entity_id', $entity_id) ->execute(); foreach ($result as $row) { - $counts[$content_type][$content_id][$row->name] = $row->count; + $counts[$entity_type][$entity_id][$row->name] = $row->count; } } - return $counts[$content_type][$content_id]; + return $counts[$entity_type][$entity_id]; } /** @@ -1384,462 +530,91 @@ function flag_get_counts($content_type, $content_id, $reset = FALSE) { * @param $flag_name * The flag name for which to retrieve a flag count. * @param $reset - * Reset the internal cache and execute the SQL query another time. + * (optional) Reset the internal cache and execute the SQL query another time. + * + * @deprecated In Drupal 8. + * @todo Move flag_get_flag_counts() into FlagCountService. */ function flag_get_flag_counts($flag_name, $reset = FALSE) { - static $counts; + $counts = &drupal_static(__FUNCTION__); if ($reset) { $counts = array(); } if (!isset($counts[$flag_name])) { $flag = flag_get_flag($flag_name); - $counts[$flag_name] = db_select('flag_count', 'fc')->fields('fc', array('fcid')) - ->condition('fid', $flag->fid)->countQuery()->execute()->fetchField(); + $counts[$flag_name] = db_select('flag_counts', 'fc') + ->fields('fc', array('flagging_id')) + ->condition('fid', $flag->fid) + ->countQuery() + ->execute() + ->fetchField(); } return $counts[$flag_name]; } /** - * Load a single flag either by name or by flag ID. - * - * @param $name - * The the flag name. - * @param $fid - * The the flag id. - */ -function flag_get_flag($name = NULL, $fid = NULL) { - $flags = flag_get_flags(); - if (isset($name)) { - if (isset($flags[$name])) { - return $flags[$name]; - } - } - elseif (isset($fid)) { - foreach ($flags as $flag) { - if ($flag->fid == $fid) { - return $flag; - } - } - } - return FALSE; -} - -/** - * List all flags available. - * - * If node type or account are entered, a list of all possible flags will be - * returned. - * - * @param $content_type - * Optional. The type of content for which to load the flags. Usually 'node'. - * @param $content_subtype - * Optional. The node type for which to load the flags. - * @param $account - * Optional. The user accont to filter available flags. If not set, all - * flags for will this node will be returned. - * @param $reset - * Optional. Reset the internal query cache. - * - * @return $flags - * An array of the structure [fid] = flag_object. - */ -function flag_get_flags($content_type = NULL, $content_subtype = NULL, $account = NULL, $reset = FALSE) { - static $flags; - - // Retrieve a list of all flags, regardless of the parameters. - if (!isset($flags) || $reset) { - $flags = array(); - - // Database flags. - $query = db_select('flags', 'f'); - $query->leftJoin('flag_types', 'fn', 'fn.fid = f.fid'); - $result = $query - ->fields('f', array('fid', 'content_type', 'name', 'title', 'global', 'options')) - ->fields('fn', array('type')) - ->execute(); - foreach ($result as $row) { - if (!isset($flags[$row->name])) { - $flags[$row->name] = flag_flag::factory_by_row($row); - } - else { - $flags[$row->name]->types[] = $row->type; - } - } - - // Add code-based flags provided by modules. - $default_flags = flag_get_default_flags(); - foreach ($default_flags as $name => $default_flag) { - // Insert new enabled flags into the database to give them an FID. - if ($default_flag->status && !isset($flags[$name])) { - $default_flag->save(); - $flags[$name] = $default_flag; - } - - if (isset($flags[$name])) { - // Ensure overridden flags are associated with their parent module. - $flags[$name]->module = $default_flag->module; - - // Update the flag with any properties that are "locked" by the code version. - if (isset($default_flag->locked)) { - $flags[$name]->locked = $default_flag->locked; - foreach ($default_flag->locked as $property) { - $flags[$name]->$property = $default_flag->$property; - } - } - } - } - - // Allow modules implementing hook_flag_alter(&$flag) to modify each flag. - foreach ($flags as $flag) { - drupal_alter('flag', $flag); - } - } - - // Make a variable copy to filter types and account. - $filtered_flags = $flags; - - // Filter out flags based on type and subtype. - if (isset($content_type) || isset($content_subtype)) { - foreach ($filtered_flags as $name => $flag) { - if (!_flag_content_enabled($flag, $content_type, $content_subtype)) { - unset($filtered_flags[$name]); - } - } - } - - // Filter out flags based on account permissions. - if (isset($account) && $account->uid != 1) { - foreach ($filtered_flags as $name => $flag) { - // We test against the 'flag' action, which is the minimum permission to - // use a flag. - if (!$flag->user_access('flag', $account)) { - unset($filtered_flags[$name]); - } - } - } - - return $filtered_flags; -} - -/** - * Utility function: Checks whether a flag applies to a certain type, and - * possibly subtype, of content. - * - * @param $content_type - * The type of content being checked, usually "node". - * @param $content_subtype - * The subtype (node type) being checked. + * Remove all flagged entities from a flag. * - * @return - * TRUE if the flag is enabled for this type and subtype. - */ -function _flag_content_enabled($flag, $content_type, $content_subtype = NULL) { - $return = $flag->content_type == $content_type && (!isset($content_subtype) || in_array($content_subtype, $flag->types)); - return $return; -} - -/** - * Retrieve a list of flags defined by modules. - * - * @param $include_disabled - * Unless specified, only enabled flags will be returned. - * @return - * An array of flag prototypes, not usable for flagging. Use flag_get_flags() - * if needing to perform a flagging with any enabled flag. - */ -function flag_get_default_flags($include_disabled = FALSE) { - $default_flags = array(); - $flag_status = variable_get('flag_default_flag_status', array()); - - foreach (module_implements('flag_default_flags') as $module) { - $function = $module . '_flag_default_flags'; - foreach ($function() as $flag_name => $flag_info) { - // Backward compatibility: old exported default flags have their names - // in $flag_info instead, so we use the += operator to not overwrite it. - $flag_info += array( - 'name' => $flag_name, - 'module' => $module, - ); - $flag = flag_flag::factory_by_array($flag_info); - - // Disable flags that are not at the current API version. - if (!$flag->is_compatible()) { - $flag->status = FALSE; - } - - // Add flags that have been enabled. - if ((!isset($flag_status[$flag->name]) && (!isset($flag->status) || $flag->status)) || !empty($flag_status[$flag->name])) { - $flag->status = TRUE; - $default_flags[$flag->name] = $flag; - } - // Add flags that have been disabled. - elseif ($include_disabled) { - $flag->status = FALSE; - $default_flags[$flag->name] = $flag; - } - } - } - - return $default_flags; -} - -/** - * Get all flagged content in a flag. + * @param $flag + * The flag object. + * @param $entity_id + * (optional) The entity ID on which all flaggings will be removed. If left + * empty, this will remove all of this flag's entities. * - * @param - * The flag name for which to retrieve flagged content. + * @deprecated In Drupal 8. + * @todo Move to Flag::reset(). */ -function flag_get_flagged_content($flag_name) { - $return = array(); - $flag = flag_get_flag($flag_name); - $result = db_select('flag_content', 'fc') +function flag_reset_flag($flag, $entity_id = NULL) { + $query = db_select('flagging', 'fc') ->fields('fc') - ->condition('fid', $flag->fid) - ->execute(); - foreach ($result as $row) { - $return[] = $row; - } - return $return; -} - -/** - * Get content ID from a flag content ID. - * - * @param $fcid - * The flag content ID for which to look up the content ID. - */ -function flag_get_content_id($fcid) { - return db_select('flag_content', 'fc') - ->fields('fc', array('content_id')) - ->condition('fcid', $fcid) - ->execute() - ->fetchField(); -} - -/** - * Find what a user has flagged, either a single node or on the entire site. - * - * @param $content_type - * The type of content that will be retrieved. Usually 'node'. - * @param $content_id - * Optional. The content ID to check for flagging. If none given, all - * content flagged by this user will be returned. - * @param $uid - * Optional. The user ID whose flags we're checking. If none given, the - * current user will be used. - * @param $sid - * Optional. The user SID (provided by Session API) whose flags we're - * checking. If none given, the current user will be used. The SID is 0 for - * logged in users. - * @param $reset - * Reset the internal cache and execute the SQL query another time. - * - * @return $flags - * If returning a single item's flags (that is, when $content_id isn't NULL), - * an array of the structure - * [flag_name] => (fcid => [fcid], uid => [uid], content_id => [content_id], timestamp => [timestamp], ...) - * - * If returning all items' flags, an array of arrays for each flag: - * [flag_name] => [content_id] => Object from above. - * - */ -function flag_get_user_flags($content_type, $content_id = NULL, $uid = NULL, $sid = NULL, $reset = FALSE) { - static $flagged_content; - - if ($reset) { - $flagged_content = array(); - if (!isset($content_type)) { - return; - } - } - - $uid = !isset($uid) ? $GLOBALS['user']->uid : $uid; - $sid = !isset($sid) ? flag_get_sid($uid) : $sid; - - if (isset($content_id)) { - if (!isset($flagged_content[$uid][$sid][$content_type][$content_id])) { - $flag_names = _flag_get_flag_names(); - $flagged_content[$uid][$sid][$content_type][$content_id] = array(); - $result = db_select('flag_content', 'fc') - ->fields('fc') - ->condition('content_type', $content_type) - ->condition('content_id', $content_id) - ->condition(db_or() - ->condition('uid', $uid) - ->condition('uid', 0) - ) - ->condition('sid', $sid) - ->execute(); - - foreach ($result as $flag_content) { - $flagged_content[$uid][$sid][$content_type][$content_id][$flag_names[$flag_content->fid]] = $flag_content; - } - } - return $flagged_content[$uid][$sid][$content_type][$content_id]; - } - - else { - if (!isset($flagged_content[$uid][$sid][$content_type]['all'])) { - $flag_names = _flag_get_flag_names(); - $flagged_content[$uid][$sid][$content_type]['all'] = array(); - $result = db_select('flag_content', 'fc') - ->fields('fc') - ->condition('content_type', $content_type) - ->condition(db_or() - ->condition('uid', $uid) - ->condition('uid', 0) - ) - ->condition('sid', $sid) - ->execute(); - foreach ($result as $flag_content) { - $flagged_content[$uid][$sid][$content_type]['all'][$flag_names[$flag_content->fid]][$flag_content->content_id] = $flag_content; - } - } - return $flagged_content[$uid][$sid][$content_type]['all']; - } - -} + ->condition('fid', $flag->fid); -/** - * Return a list of users who have flagged a piece of content. - * - * @param $content_type - * The type of content that will be retrieved. Usually 'node'. - * @param $content_id - * The content ID to check for flagging. - * @param $flag_name - * Optional. The name of a flag if wanting a list specific to a single flag. - * @param $reset - * Reset the internal cache of flagged content. - * @return - * If no flag name is given, an array of flagged content, keyed by the user - * ID that flagged the content. Each flagged content array is structured as - * an array of flag information for each flag, keyed by the flag name. If - * a flag name is specified, only the information for that flag is returned. - */ -function flag_get_content_flags($content_type, $content_id, $flag_name = NULL, $reset = FALSE) { - static $content_flags; - - if (!isset($content_flags[$content_type][$content_id]) || $reset) { - $flag_names = _flag_get_flag_names(); - $result = db_select('flag_content', 'fc') - ->fields('fc') - ->condition('content_type', $content_type) - ->condition('content_id', $content_id) - ->orderBy('timestamp', 'DESC') - ->execute(); - foreach ($result as $flag_content) { - // Build a list of flaggings for all flags by user. - $content_flags[$content_type][$content_id]['users'][$flag_content->uid][$flag_names[$flag_content->fid]] = $flag_content; - // Build a list of flaggings for each individual flag. - $content_flags[$content_type][$content_id]['flags'][$flag_names[$flag_content->fid]][$flag_content->uid] = $flag_content; - } + if ($entity_id) { + $query->condition('entity_id', $entity_id); } - return isset($flag_name) ? $content_flags[$content_type][$content_id]['flags'][$flag_name] : $content_flags[$content_type][$content_id]['users']; -} - -/** - * A utility function for outputting a flag link. - * - * You should call this function from your template when you want to put the - * link on the page yourself. For example, you could call this function from - * your 'node.tpl.php': - * - * nid); ?> - * - * @param $flag_name - * The "machine readable" name of the flag; e.g. 'bookmarks'. - * @param $content_id - * The content ID to check for flagging. This is usually a node ID. - */ -function flag_create_link($flag_name, $content_id) { - $flag = flag_get_flag($flag_name); - if (!$flag) { - // Flag does not exist. - return; - } - if (!$flag->access($content_id) && (!$flag->is_flagged($content_id) || !$flag->access($content_id, 'flag'))) { - // User has no permission to use this flag. - return; + $result = $query->execute()->fetchAllAssoc('flagging_id', PDO::FETCH_ASSOC); + $rows = array(); + foreach ($result as $row) { + $rows[] = $row; } - return $flag->theme($flag->is_flagged($content_id) ? 'unflag' : 'flag', $content_id); -} + \Drupal::moduleHandler()->invokeAll('flag_reset', array($flag, $entity_id, $rows)); -/** - * Return an array of link types provided by modules. - */ -function flag_get_link_types($reset = FALSE) { - static $link_types; - - if (!isset($link_types) || $reset) { - $link_types = array(); - foreach (module_implements('flag_link_types') as $module) { - $module_types = module_invoke($module, 'flag_link_types'); - foreach ($module_types as $type_name => $info) { - $link_types[$type_name] = $info + array( - 'module' => $module, - 'name' => $type_name, - 'title' => '', - 'description' => '', - 'options' => array(), - 'uses standard js' => TRUE, - 'uses standard css' => TRUE, - ); - } - } - drupal_alter('flag_link_types', $link_types); + $query = db_delete('flagging')->condition('fid' , $flag->fid); + // Update the flag_counts table. + $count_query = db_delete('flag_counts')->condition('fid', $flag->fid); + if ($entity_id) { + $query->condition('entity_id', $entity_id); + $count_query->condition('entity_id', $entity_id); } - - return $link_types; -} - -/** - * Get a private token used to protect links from spoofing - CSRF. - */ -function flag_get_token($content_id) { - // Anonymous users get a less secure token, since it must be the same for all - // anonymous users on the entire site to work with page caching. - return ($GLOBALS['user']->uid) ? drupal_get_token($content_id) : md5(drupal_get_private_key() . $content_id); -} - -/** - * Check to see if a token value matches the specified node. - */ -function flag_check_token($token, $content_id) { - return flag_get_token($content_id) == $token; + $count_query->execute(); + return $query->execute(); } /** - * Set the Session ID for a user. Utilizes the Session API module. - * - * This function is only called in flag_init(), to set the current user's - * SID in case the user logs in during this request. + * Implements hook_entity_operation(). */ -function flag_set_sid($uid = NULL) { - static $sids = array(); +function flag_entity_operation(\Drupal\Core\Entity\EntityInterface $entity) { + $operations = []; - if (!isset($uid)) { - $uid = $GLOBALS['user']->uid; - } - - if (!isset($sids[$uid])) { - if (module_exists('session_api') && session_api_available() && $uid == 0) { - $sids[$uid] = session_api_get_sid(); + if ($entity instanceof \Drupal\flag\FlagInterface) { + if (!$entity->isEnabled()) { + $operations['enable'] = [ + 'title' => t('Enable'), + 'url' => $entity->urlInfo('enable'), + 'weight' => 50, + ]; } else { - $sids[$uid] = 0; + $operations['disable'] = [ + 'title' => t('Disable'), + 'url' => $entity->urlInfo('disable'), + 'weight' => 50, + ]; } } - return $sids[$uid]; -} - -/** - * Get the Session ID for a user. Utilizes the Session API module. - */ -function flag_get_sid($uid = NULL) { - return flag_set_sid($uid); + return $operations; } diff --git a/flag.permissions.yml b/flag.permissions.yml new file mode 100644 index 0000000..4f4a6a3 --- /dev/null +++ b/flag.permissions.yml @@ -0,0 +1,7 @@ +administer flags: + title: 'Administer Flags' + decription: 'Define and manage Flags and Flag settings.' + restrict access: TRUE + +permission_callbacks: + - Drupal\flag\FlagPermissions::permissions \ No newline at end of file diff --git a/flag.routing.yml b/flag.routing.yml new file mode 100644 index 0000000..43eec97 --- /dev/null +++ b/flag.routing.yml @@ -0,0 +1,119 @@ +flag.add_page: + path: '/admin/structure/flags/add' + defaults: + _form: '\Drupal\flag\Form\FlagAddPageForm' + _title: 'Add New Flag' + requirements: + _permission: 'administer flags' + +flag.list: + path: '/admin/structure/flags' + defaults: + _entity_list: 'flag' + _title: 'Flags' + requirements: + _permission: 'administer flags' + +flag.add: + path: '/admin/structure/flags/add/{entity_type}' + defaults: + _entity_form: flag.add + _title: 'Add New Flag' + requirements: + _permission: 'administer flags' + +flag.edit: + path: '/admin/structure/flags/manage/{flag}' + defaults: + _entity_form: flag.edit + _title: 'Edit Flag' + requirements: + _entity_access: 'flag.update' + +flag.delete: + path: '/admin/structure/flags/manage/{flag}/delete' + defaults: + _entity_form: flag.delete + _title: 'Delete Flag' + requirements: + _entity_access: 'flag.update' + +flag_link_flag.html: + path: '/flag/flag/{flag_id}/{entity_id}' + defaults: + _controller: '\Drupal\flag\Controller\ReloadLinkController::flag' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkflag' + _format: 'html' + +flag.link_flag.json: + path: '/flag/flag/{flag_id}/{entity_id}' + defaults: + _controller: '\Drupal\flag\Controller\AJAXLinkController::flag' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkflag' + _format: 'json' + +flag_link_unflag.html: + path: '/flag/unflag/{flag_id}/{entity_id}' + defaults: + _controller: '\Drupal\flag\Controller\ReloadLinkController::unflag' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkUnflag' + _format: 'html' + +flag.link_unflag.json: + path: '/flag/unflag/{flag_id}/{entity_id}' + defaults: + _controller: '\Drupal\flag\Controller\AJAXLinkController::unflag' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkUnflag' + _format: 'json' + +flag.confirm_flag: + path: '/flag/confirm/flag/{flag_id}/{entity_id}' + defaults: + _form: '\Drupal\flag\Form\FlaggingConfirmForm' + _title: 'Flag Content' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkflag' + +flag.confirm_unflag: + path: '/flag/confirm/unflag/{flag_id}/{entity_id}' + defaults: + _form: '\Drupal\flag\Form\FlaggingConfirmForm' + _title: 'Flag Content' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkUnflag' + +flag.field_entry: + path: '/flag/details/flag/{flag_id}/{entity_id}' + defaults: + _controller: '\Drupal\flag\Controller\FieldEntryFormController::flag' + _title_callback: '\Drupal\flag\Controller\FieldEntryFormController::flagTitle' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkflag' + +flag.field_entry.edit: + path: '/flag/details/edit/{flag_id}/{entity_id}' + defaults: + _controller: '\Drupal\flag\Controller\FieldEntryFormController::edit' + _title_callback: '\Drupal\flag\Controller\FieldEntryFormController::editTitle' + requirements: + _custom_access: '\Drupal\flag\FlaggingAccessController::checkflag' + +flag.enable: + path: '/flag/enable/{flag}' + defaults: + _form: '\Drupal\flag\Form\FlagDisableConfirmForm' + _title: 'Enable Flag?' + requirements: + _permission: 'administer flags' + +flag.disable: + path: '/flag/disable/{flag}' + defaults: + _form: '\Drupal\flag\Form\FlagDisableConfirmForm' + _title: 'Disable Flag?' + requirements: + _permission: 'administer flags' diff --git a/flag.rules.inc b/flag.rules.inc deleted file mode 100644 index 10d2976..0000000 --- a/flag.rules.inc +++ /dev/null @@ -1,407 +0,0 @@ - array( - 'label' => t('flag'), - 'ui class' => 'FlagRulesUIClass', - 'wrapper class' => 'FlagRulesDataWrapper', - 'wrap' => TRUE, - ), - ); -} - -/** - * A custom wrapper class for flags to be used with Rules. - * @ingroup rules - */ -class FlagRulesDataWrapper extends RulesIdentifiableDataWrapper implements RulesDataWrapperSavableInterface { - - protected function extractIdentifier($flag) { - return $flag->name; - } - - protected function load($name) { - return flag_get_flag($name); - } - - public function save() { - $flag = $this->value(); - $flag->save(); - } - - public function validate($value) { - if (isset($value) && is_string($value)) { - return TRUE; - } - elseif (isset($value) && is_object($value) && $value instanceof flag_flag) { - return TRUE; - } - return parent::validate($value); - } -} - -/** - * UI for inputing flags. - * @ingroup rules - */ -class FlagRulesUIClass extends RulesDataUI implements RulesDataDirectInputFormInterface { - - public static function getDefaultMode() { - return 'input'; - } - - public static function inputForm($name, $info, $settings, RulesPlugin $element) { - $options = _flag_rules_flags_options(isset($info['flag_type']) ? $info['flag_type'] : NULL); - $header = array( - 'title' => t('Flag:'), - 'type' => t('The flag type'), - 'global' => t('Is the flag global?'), - ); - $settings += array($name => isset($info['default value']) ? $info['default value'] : ''); - - $form[$name] = array( - '#type' => 'tableselect', - '#header' => $header, - '#options' => $options, - '#required' => empty($info['optional']), - '#multiple' => FALSE, - '#default_value' => $settings[$name], - '#emtpy' => t('There is no suiting flag available.') - ); - return $form; - } - - public static function render($value) { - $flag = flag_get_flag($value); - return array( - 'content' => array('#markup' => check_plain($flag->get_title())), - '#attributes' => array('class' => array('rules-parameter-flag')), - ); - } -} - -function _flag_rules_flags_options($flag_type = NULL) { - $flags = flag_get_flags(); - $options = array(); - foreach ($flags as $flag) { - if (!isset($flag_type) || $flag->content_type == $flag_type) { - $options[$flag->name] = array( - 'title' => $flag->get_title(), - 'type' => $flag->content_type, - 'global' => $flag->global ? t('Yes') : t('No'), - ); - } - } - return $options; -} - -/** - * Implements hook_rules_event_info(). - */ -function flag_rules_event_info() { - $items = array(); - - $flags = flag_get_flags(); - foreach ($flags as $flag) { - // We only support flags on entities. - if ($info = entity_get_info($flag->content_type)) { - $variables = array( - 'flag' => array( - 'type' => 'flag', - 'label' => t('flag'), - 'flag_type' => $flag->content_type, - ), - 'flagged_' . $flag->content_type => array( - 'type' => $flag->content_type, - 'label' => $info['label'], - ), - 'flagging_user' => array( - 'type' => 'user', - 'label' => t('flagging user'), - ), - ); - - // For each flag we define two events. - $items['flag_flagged_' . $flag->name] = array( - 'group' => t('Flag'), - 'label' => t('A @flag-type has been flagged, under "@flag-title"', array('@flag-title' => $flag->get_title(), '@flag-type' => t($flag->content_type))), - 'variables' => $variables, - 'access callback' => 'flag_rules_integration_access', - ); - $items['flag_unflagged_' . $flag->name] = array( - 'group' => t('Flag'), - 'label' => t('A @flag-type has been unflagged, under "@flag-title"', array('@flag-title' => $flag->get_title(), '@flag-type' => t($flag->content_type))), - 'variables' => $variables, - 'access callback' => 'flag_rules_integration_access', - ); - } - } - return $items; -} - -/** - * Implements hook_rules_action_info(). - */ -function flag_rules_action_info() { - $items = array( - 'flag_trim' => array( - 'label' => t('Trim a flag'), - 'base' => 'flag_rules_action_trim', - 'parameter' => array( - 'flag' => array( - 'type' => 'flag', - 'label' => t('Flag'), - ), - 'flagging_user' => array( - 'type' => 'user', - 'label' => t('User whose flag to trim'), - 'description' => t('For non-global flags, this is the user whose flag to trim. (For global flags, this argument is ignored.)'), - ), - 'cutoff_size' => array( - 'type' => 'integer', - 'label' => t('Flag queue size'), - 'description' => t('The maximum number of objects to keep in the queue. Newly flagged objects will be kept; older ones will be removed. Tip: by typing "1" here you implement a singleton.'), - ), - ), - 'group' => t('Flag'), - 'access callback' => 'flag_rules_integration_access', - ), - ); - $param_defaults = array( - 'flagging_user' => array( - 'type' => 'user', - 'label' => t('User on whose behalf to flag'), - 'description' => t('For non-global flags, this is the user on whose behalf to flag the object. In addition, if checked below, the access permissions to the flag are checked against this user.'), - ), - 'permission_check' => array( - 'type' => 'boolean', - 'label' => t('Permission check'), - 'description' => t('Whether to check access permissions against the user on whose behalf to flag.'), - 'restriction' => 'input', - ), - ); - foreach (flag_get_types() as $type) { - $flag = flag_create_handler($type); - $entity_info = entity_get_info($type); - $items += array( - 'flag_flag'. $type => array( - 'label' => t('Flag a @type', array('@type' => $type)), - 'base' => 'flag_rules_action_flag', - 'parameter' => array( - 'flag' => array( - 'type' => 'flag', - 'label' => t('Flag'), - 'flag_type' => $type, - 'description' => t('The flag to check for.') - ), - $type => array( - 'type' => $type, - 'label' => isset($entity_info[$type]['label']) ? $entity_info[$type]['label'] : $type, - ), - ) + $param_defaults, - 'group' => t('Flag'), - 'access callback' => 'flag_rules_integration_access', - ), - 'flag_unflag'. $type => array( - 'label' => t('Unflag a @type', array('@type' => $type)), - 'base' => 'flag_rules_action_unflag', - 'parameter' => array( - 'flag' => array( - 'type' => 'flag', - 'label' => t('Flag'), - 'flag_type' => $type, - 'description' => t('The flag to check for.') - ), - $type => array( - 'type' => $type, - 'label' => isset($entity_info[$type]['label']) ? $entity_info[$type]['label'] : $type, - ), - ) + $param_defaults, - 'group' => t('Flag'), - 'access callback' => 'flag_rules_integration_access', - ), - ); - $items['flag_fetch_users_' . $type] = array( - 'label' => t('Fetch users who have flagged a @type', array('@type' => $type)), - 'base' => 'flag_rules_action_fetch_users', - 'parameter' => array( - 'flag' => array( - 'type' => 'flag', - 'label' => t('Flag'), - 'flag_type' => $type, - 'description' => t('Choose the flag for which to fetch the users.'), - ), - $type => array( - 'type' => $type, - 'label' => isset($entity_info[$type]['label']) ? $entity_info[$type]['label'] : $type, - ), - ), - 'provides' => array( - 'users' => array( - 'label' => t('Users who flagged'), - 'type' => 'list', - ), - ), - 'group' => t('Flag'), - 'access callback' => 'flag_rules_integration_access', - ); - } - return $items; -} - -/** - * Base action implementation: Flag. - */ -function flag_rules_action_flag($flag, $entity, $flagging_user, $permissions_check) { - $flag->flag('flag', $flag->get_content_id($entity), $flagging_user, $permissions_check); -} - -/** - * Base action implementation: Unflag. - */ -function flag_rules_action_unflag($flag, $entity, $flagging_user, $permissions_check) { - $flag->flag('unflag', $flag->get_content_id($entity), $flagging_user, $permissions_check); -} - -/** - * Base action implementation: Trim flag. - */ -function flag_rules_action_trim($flag, $flagging_user, $cutoff_size) { - flag_trim_flag($flag, $flagging_user, $cutoff_size); -} - -/** - * Base action implementation: Fetch users who flagged an entity. - */ -function flag_rules_action_fetch_users($flag, $entity) { - $result = db_select('flag_content', 'fc') - ->fields('fc', array('uid')) - ->condition('content_type', $flag->content_type) - ->condition('content_id', $flag->get_content_id($entity)) - ->condition('fid', $flag->fid) - ->execute(); - $uids = $result->fetchCol(); - // Filter out anonymous users. - return array('users' => array_filter($uids)); -} - -/** - * Implements hook_rules_condition_info(). - */ -function flag_rules_condition_info() { - $items = array(); - foreach (flag_get_types() as $type) { - $flag = flag_create_handler($type); - $entity_info = entity_get_info($type); - $label = isset($entity_info[$type]['label']) ? $entity_info[$type]['label'] : $type; - $items += array( - 'flag_threshold_'. $type => array( - 'label' => drupal_ucfirst(t('@type has flagging count', array('@type' => $label))), - 'base' => 'flag_rules_condition_threshold', - 'parameter' => array( - 'flag' => array( - 'type' => 'flag', - 'label' => t('Flag'), - 'flag_type' => $type, - 'description' => t('The flag to check for.') - ), - $type => array( - 'type' => $type, - 'label' => $label, - ), - 'number' => array( - 'type' => 'integer', - 'label' => t('Number'), - 'description' => t('The number against which to test the number of times the object is flagged. For example, if you type "3" here, and choose "Greater than" for the operator, then this condition will return TRUE if the object is flagged more than three times.'), - ), - 'operator' => array( - 'type' => 'text', - 'label' => t('Comparison operator'), - 'options list' => 'flag_rules_condition_threshold_operator_options', - 'restriction' => 'input', - 'default value' => '=', - 'optional' => TRUE, - ), - ), - 'group' => t('Flag'), - 'access callback' => 'flag_rules_integration_access', - ), - 'flag_flagged_'. $type => array( - 'label' => drupal_ucfirst(t('@type is flagged', array('@type' => $label))), - 'base' => 'flag_rules_condition_flagged', - 'parameter' => array( - 'flag' => array( - 'type' => 'flag', - 'label' => t('Flag'), - 'flag_type' => $type, - 'description' => t('The flag to check for.') - ), - $type => array( - 'type' => $type, - 'label' => $label, - ), - 'user' => array( - 'type' => 'user', - 'label' => t('User on whose behalf to check'), - 'description' => t('For non-global flags, this is the user on whose behalf the flag is checked.'), - ), - ), - 'group' => t('Flag'), - 'access callback' => 'flag_rules_integration_access', - ), - ); - } - return $items; -} - -/** - * Options list callback for the operator parameter of the flagging threshold condition. - */ -function flag_rules_condition_threshold_operator_options() { - return array( - '>' => t('Greater than'), - '>=' => t('Greater than or equal'), - '=' => t('Equal to'), - '<=' => t('Less than or equal'), - '<' => t('Less than'), - ); -} - -/** - * Condition: Check flagging count. - */ -function flag_rules_condition_threshold($flag, $entity, $number, $operator = '=') { - $count = $flag->get_count($flag->get_content_id($entity)); - - switch ($operator) { - case '>' : return $count > $number; - case '>=': return $count >= $number; - case '=' : return $count == $number; - case '<' : return $count < $number; - case '<=': return $count <= $number; - } -} - -/** - * Condition: Flag is flagged. - */ -function flag_rules_condition_flagged($flag, $entity, $account) { - return $flag->is_flagged($flag->get_content_id($entity), $account->uid); -} - -/** - * Rules integration access callback. - */ -function flag_rules_integration_access($type, $name) { - return user_access('administer flags'); -} diff --git a/flag.services.yml b/flag.services.yml new file mode 100644 index 0000000..8fa2776 --- /dev/null +++ b/flag.services.yml @@ -0,0 +1,10 @@ +services: + plugin.manager.flag.flagtype: + class: Drupal\flag\FlagTypePluginManager + arguments: ['@container.namespaces', '@cache.default', '@module_handler'] + plugin.manager.flag.linktype: + class: Drupal\flag\ActionLinkPluginManager + arguments: ['@container.namespaces', '@cache.default', '@module_handler'] + flag: + class: Drupal\flag\FlagService + arguments: ['@plugin.manager.flag.flagtype', '@event_dispatcher', '@entity.query', '@current_user', '@entity.manager'] diff --git a/includes/flag.token.inc b/flag.tokens.inc similarity index 51% rename from includes/flag.token.inc rename to flag.tokens.inc index 5cd1f4b..52bc66f 100644 --- a/includes/flag.token.inc +++ b/flag.tokens.inc @@ -6,7 +6,7 @@ */ /** - * Implements of hook_token_info(). + * Implements hook_token_info(). */ function flag_token_info() { $types = array(); @@ -27,7 +27,23 @@ function flag_token_info() { 'description' => t('The human-readable flag title.'), ); - // Flage action tokens. + // Flagging tokens. + // + // Attached fields are exposed as tokens via some contrib module, but we + // need to expose other fields ourselves. Currently, 'date' is the only such + // field we expose. + $types['flagging'] = array( + 'name' => t('Flaggings'), + 'description' => t('Tokens related to flaggings.'), + 'needs-data' => 'flagging', + ); + $tokens['flagging']['date'] = array( + 'name' => t('Flagging date'), + 'description' => t('The date an item was flagged.'), + 'type' => 'date', + ); + + // Flag action tokens. $types['flag-action'] = array( 'name' => t('Flag actions'), 'description' => t('Tokens available in response to a flag action being executed by a user.'), @@ -37,21 +53,21 @@ function flag_token_info() { 'name' => t('Flag action'), 'description' => t('The flagging action taking place, either "flag" or "unflag".'), ); - $tokens['flag-action']['content-url'] = array( - 'name' => t('Flag content URL'), - 'description' => t('The URL of the content being flagged.'), + $tokens['flag-action']['entity-url'] = array( + 'name' => t('Flag entity URL'), + 'description' => t('The URL of the entity being flagged.'), ); - $tokens['flag-action']['content-title'] = array( - 'name' => t('Flag content title'), - 'description' => t('The title of the content being flagged.'), + $tokens['flag-action']['entity-title'] = array( + 'name' => t('Flag entity title'), + 'description' => t('The title of the entity being flagged.'), ); - $tokens['flag-action']['content-type'] = array( - 'name' => t('Flag content type'), - 'description' => t('The type of content being flagged, such as node or comment.'), + $tokens['flag-action']['entity-type'] = array( + 'name' => t('Flag entity type'), + 'description' => t('The type of entity being flagged, such as node or comment.'), ); - $tokens['flag-action']['content-id'] = array( - 'name' => t('Flag content ID'), - 'description' => t('The ID of content being flagged, may be a nid or cid.'), + $tokens['flag-action']['entity-id'] = array( + 'name' => t('Flag entity ID'), + 'description' => t('The ID of entity being flagged, such as a nid or cid.'), ); $tokens['flag-action']['count'] = array( 'name' => t('Flag count'), @@ -59,12 +75,16 @@ function flag_token_info() { ); // Add tokens for the flag count available at the node/comment/user level. - foreach (flag_get_types() as $flag_type) { - $flags = flag_get_flags($flag_type); + foreach (array_keys(\Drupal::service('flag')->fetchDefinition()) as $flag_type) { + $flags = \Drupal::service('flag')->getFlags($flag_type); foreach ($flags as $flag) { - $tokens[$flag_type]['flag-'. str_replace('_', '-', $flag->name) .'-count'] = array( - 'name' => t('@flag flag count', array('@flag' => $flag->get_title())), - 'description' => t('Total flag count for flag @flag', array('@flag' => $flag->get_title())), + $tokens[$flag_type]['flag-' . str_replace('_', '-', $flag->id()) . '-count'] = array( + 'name' => t('@flag flag count', array('@flag' => $flag->label())), + 'description' => t('Total flag count for flag @flag', array('@flag' => $flag->label())), + ); + $tokens[$flag_type]['flag-' . str_replace('_', '-', $flag->id()) . '-link'] = array( + 'name' => t('@flag flag link', array('@flag' => $flag->label())), + 'description' => t('Flag/unflag link for @flag', array('@flag' => $flag->label())), ); } } @@ -79,8 +99,10 @@ function flag_token_info() { * Implements hook_tokens(). */ function flag_tokens($type, $tokens, array $data = array(), array $options = array()) { + /* $replacements = array(); $sanitize = !empty($options['sanitize']); + $langcode = isset($options['language']) ? $options['language']->language : NULL; if ($type == 'flag' && !empty($data['flag'])) { $flag = $data['flag']; @@ -89,12 +111,26 @@ function flag_tokens($type, $tokens, array $data = array(), array $options = arr case 'name': $replacements[$original] = $sanitize ? check_plain($flag->name) : $flag->name; break; + case 'title': $replacements[$original] = $sanitize ? check_plain($flag->get_title()) : $flag->get_title(); break; } } } + elseif ($type == 'flagging' && !empty($data['flagging'])) { + $flagging = $data['flagging']; + foreach ($tokens as $name => $original) { + switch ($name) { + case 'date': + $replacements[$original] = format_date($flagging->timestamp, 'medium', '', NULL, $langcode); + break; + } + } + if ($date_tokens = token_find_with_prefix($tokens, 'date')) { + $replacements += token_generate('date', $date_tokens, array('date' => $flagging->timestamp), $options); + } + } elseif ($type == 'flag-action' && !empty($data['flag-action'])) { $action = $data['flag-action']; foreach ($tokens as $name => $original) { @@ -102,53 +138,63 @@ function flag_tokens($type, $tokens, array $data = array(), array $options = arr case 'action': $replacements[$original] = $action->action; break; - case 'content-url': - $replacements[$original] = $sanitize ? check_url($action->content_url) : $action->content_url; + + case 'entity-url': + $replacements[$original] = $sanitize ? check_url($action->entity_url) : $action->entity_url; break; - case 'content-title': - $replacements[$original] = $sanitize ? check_plain($action->content_title) : $action->content_title; + + case 'entity-title': + $replacements[$original] = $sanitize ? check_plain($action->entity_title) : $action->entity_title; break; - case 'content-type': - $replacements[$original] =$action->content_type; + + case 'entity-type': + $replacements[$original] = $action->entity_type; break; - case 'content-id': - $replacements[$original] =$action->content_id; + + case 'entity-id': + $replacements[$original] = $action->entity_id; break; + case 'count': - $replacements[$original] =$action->count; + $replacements[$original] = $action->count; break; } } } - if (isset($data[$type]) && in_array($type, flag_get_types())) { - $flags = flag_get_flags($type); + if (isset($data[$type]) && in_array($type, \Drupal::service('flag')->fetchDefinition())) { + $flags = \Drupal::service('flag')->getFlags($type); $object = $data[$type]; foreach ($flags as $flag) { foreach ($tokens as $name => $original) { - $flag_token = 'flag-'. str_replace('_', '-', $flag->name) .'-count'; - if ($name == $flag_token) { - $replacements[$original] = $flag->get_count($flag->get_content_id($object)); + $flag_count_token = 'flag-' . str_replace('_', '-', $flag->name) . '-count'; + $flag_link_token = 'flag-' . str_replace('_', '-', $flag->name) . '-link'; + if ($name == $flag_count_token) { + $replacements[$original] = $flag->get_count($flag->get_entity_id($object)); + } + elseif ($name == $flag_link_token) { + $replacements[$original] = flag_create_link($flag->name, $flag->get_entity_id($object)); } } } } return $replacements; + */ } /** * Returns HTML for a tokens browser. * - * @param $variables + * @param array $variables * An associative array containing: * - types: An array naming the types of tokens to show. * - global_types: Whether to show global tokens. */ -function theme_flag_tokens_browser($variables) { +function theme_flag_tokens_browser(array $variables) { $types = $variables['types']; $global_types = $variables['global_types']; - if (module_exists('token')) { + if (\Drupal::moduleHandler()->moduleExists('token')) { return theme('token_tree', array('token_types' => $types, 'global_types' => $global_types)); } else { diff --git a/flag.views.inc b/flag.views.inc new file mode 100644 index 0000000..12e16a4 --- /dev/null +++ b/flag.views.inc @@ -0,0 +1,174 @@ + [ + 'type' => 'LEFT', + 'left_field' => 'nid', + 'field' => 'entity_id', + ], + ]; + + $data['flagging']['uid'] = [ + 'title' => t('User uid'), + 'help' => t('The user that flagged an item. If you need more fields than the uid add the "Flags: User" relationship.'), + 'relationship' => [ + 'base' => 'users', + 'title' => t('User'), + 'help' => t('Relate an item to the user that flagged it.'), + 'id' => 'standard', + 'label' => t('Flag user'), + ], + 'filter' => [ + 'id' => 'numeric', + ], + 'argument' => [ + 'id' => 'numeric', + ], + 'field' => [ + 'id' => 'user', + ], + ]; + + $data['flagging']['created'] = [ + 'title' => t('Last Flagged Time'), + 'help' => t('Display latest time the content was flagged by a user.'), + 'field' => [ + 'id' => 'date', + ], + 'sort' => [ + 'id' => 'date', + ], + 'filter' => [ + 'id' => 'date', + ], + 'argument' => [ + 'id' => 'date', + ], + ]; + + $data['flagging']['entity_id'] = [ + 'title' => t('Flaggable ID'), + 'help' => t('The unique ID of the object that has been flagged.'), + 'sort' => [ + 'id' => 'standard', + ], + 'argument' => [ + 'id' => 'FlagViewsFlaggableArgument', + ], + ]; + + // Flag content links. + $data['flagging']['link_flag'] = [ + 'field' => [ + 'title' => t('Flag Links'), + 'help' => t('Display flag/unflag link.'), + 'id' => 'flag_link', + ], + ]; + + // Specialized is null/is not null filter. + $data['flagging']['flagged'] = [ + 'title' => t('Flagged'), + 'real field' => 'uid', + 'field' => [ + 'id' => 'flag_flagged', + 'label' => t('Flagged'), + 'help' => t('A boolean field to show whether the flag is set or not.'), + ], + 'filter' => [ + 'id' => 'flag_filter', + 'label' => t('Flagged'), + 'help' => t('Filter to ensure content has or has not been flagged.'), + ], + 'sort' => [ + 'id' => 'flag_sort', + 'label' => t('Flagged'), + 'help' => t('Sort by whether entities have or have not been flagged.'), + ], + ]; + + $data['flag_counts']['count'] = [ + 'title' => t('Flag counter'), + 'help' => t('The number of times a piece of content is flagged by any user.'), + 'field' => [ + 'id' => 'numeric', + // 'click sortable' => TRUE, + ], + /*'sort' => array( + 'id' => 'groupby_numeric', + ), + 'filter' => array( + 'id' => 'numeric', + ), + 'argument' => array( + 'id' => 'numeric', + ),*/ + ]; + + $data['flag_counts']['last_updated'] = [ + 'title' => t('Time last flagged'), + 'help' => t('The time a piece of content was most recently flagged by any user.'), + 'field' => [ + 'id' => 'date', + // 'click sortable' => TRUE, + ], + /*'sort' => array( + 'id' => 'date', + ), + 'filter' => array( + 'id' => 'date', + ), + 'argument' => array( + 'id' => 'date', + ),*/ + ]; + + return $data; +} + +/** + * Implements hook_views_data_alter(). + */ +function flag_views_data_alter(array &$data) { + $flags = \Drupal::service('flag')->getFlags(); + + foreach ($flags as $fid => $flag) { + $flag_type_plugin = $flag->getFlagTypePlugin(); + $flag_type_def = $flag_type_plugin->getPluginDefinition(); + $entity_type = $flag_type_def['entity_type']; + + $info = \Drupal::entityManager()->getDefinition($entity_type); + + $base_table = $info->getBaseTable(); + if ($flag_type_def['entity_type'] == 'node') { + $base_table = $info->getDataTable(); + } + + $data[$base_table]['flag_content_rel'] = [ + 'title' => t('@entity_label flag', ['@entity_label' => $entity_type]), + 'help' => t('Limit results to only those entity flagged by a certain flag; Or display information about the flag set on a entity.'), + 'relationship' => [ + 'group' => t('Flag'), + 'label' => t('Flags'), + 'base' => 'flagging', + 'base field' => 'entity_id', + 'relationship field' => $info->getKey('id'), + 'id' => 'flag_relationship', + 'flaggable' => $entity_type, + ], + ]; + + } +} diff --git a/flag_actions.info b/flag_actions.info deleted file mode 100644 index 7fc0913..0000000 --- a/flag_actions.info +++ /dev/null @@ -1,9 +0,0 @@ -name = Flag actions -description = Execute actions on Flag events. -core = 7.x -dependencies[] = flag -package = Flags -configure = admin/structure/webform/flags - -files[] = flag.install -files[] = flag_actions.module \ No newline at end of file diff --git a/flag_actions.install b/flag_actions.install deleted file mode 100644 index a191da0..0000000 --- a/flag_actions.install +++ /dev/null @@ -1,105 +0,0 @@ - array( - 'aid' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'disp-width' => '5', - ), - 'fid' => array( - 'type' => 'int', - 'size' => 'small', - 'not null' => FALSE, - 'disp-width' => '5', - ), - 'event' => array( - 'type' => 'varchar', - 'length' => '255', - 'not null' => FALSE, - ), - 'threshold' => array( - 'type' => 'int', - 'size' => 'small', - 'not null' => TRUE, - 'default' => 0, - 'disp-width' => '5', - ), - 'repeat_threshold' => array( - 'type' => 'int', - 'size' => 'small', - 'not null' => TRUE, - 'default' => 0, - 'disp-width' => '5', - ), - 'callback' => array( - 'type' => 'varchar', - 'length' => '255', - 'not null' => TRUE, - 'default' => '', - ), - 'parameters' => array( - 'type' => 'text', - 'size' => 'big', - 'not null' => TRUE, - ), - ), - 'primary key' => array('aid'), - ); - - return $schema; -} - -/** - * Add a "repeat_threshold" value to all existing Flag actions. - */ -function flag_actions_update_6200() { - $ret = array(); - - // Add the new repeat_threshold column. - if (!db_field_exists('flag_actions', 'repeat_threshold')) { - $column = array( - 'type' => 'int', - 'size' => 'small', - 'not null' => TRUE, - 'default' => 0, - 'disp-width' => '5', - ); - db_add_field($ret, 'flag_actions', 'repeat_threshold', $column); - } - - // Update the normal threshold column to default to 0. - $column = array( - 'type' => 'int', - 'size' => 'small', - 'not null' => TRUE, - 'default' => 0, - 'disp-width' => '5', - ); - db_change_field($ret, 'flag_actions', 'threshold', 'threshold', $column); - - return $ret; -} diff --git a/flag_actions.module b/flag_actions.module deleted file mode 100644 index 6a20555..0000000 --- a/flag_actions.module +++ /dev/null @@ -1,684 +0,0 @@ - 'Actions', - 'page callback' => 'flag_actions_page', - 'access callback' => 'user_access', - 'access arguments' => array('administer actions'), - 'type' => MENU_LOCAL_TASK, - ); - $items[FLAG_ADMIN_PATH . '/actions/add'] = array( - 'title' => 'Add action', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_actions_form', NULL, 5), - 'access callback' => 'user_access', - 'access arguments' => array('administer actions'), - 'type' => MENU_CALLBACK, - ); - $items[FLAG_ADMIN_PATH . '/actions/delete'] = array( - 'title' => 'Delete action', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_actions_delete_form', 5), - 'access callback' => 'user_access', - 'access arguments' => array('administer actions'), - 'type' => MENU_CALLBACK, - ); - $items[FLAG_ADMIN_PATH . '/actions/configure'] = array( - 'title' => 'Edit action', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('flag_actions_form', 5), - 'access callback' => 'user_access', - 'access arguments' => array('administer actions'), - 'type' => MENU_CALLBACK, - ); - - return $items; -} - -/** - * Implementation of hook_theme(). - */ -function flag_actions_theme() { - return array( - 'flag_actions_page' => array( - 'variables' => array('actions' => NULL, 'form' => NULL), - ), - 'flag_actions_add_form' => array( - 'render element' => 'form', - ), - 'flag_actions_flag_form' => array( - 'render element' => 'form', - ), - ); -} - -function flag_actions_get_action($aid) { - $actions = flag_actions_get_actions(); - return $actions[$aid]; -} - -function flag_actions_get_actions($flag_name = NULL, $reset = FALSE) { - static $flag_actions; - module_load_include('inc', 'flag', 'includes/flag.actions'); - - // Get a list of all possible actions defined by modules. - $actions = module_invoke_all('action_info'); - - // Retrieve the list of user-defined flag actions. - if (!isset($flag_actions) || $reset) { - $flag_actions = array(); - $query = db_select('flag_actions', 'a'); - $query->innerJoin('flags', 'f', 'a.fid = f.fid'); - $query->addField('f', 'name', 'flag'); - $result = $query - ->fields('a') - ->execute(); - foreach ($result as $action) { - if (!isset($actions[$action->callback])) { - $actions[$action->callback] = array( - 'description' => t('Missing action "@action-callback". Module providing it was either uninstalled or disabled.', array('@action-callback' => $action->callback)), - 'configurable' => FALSE, - 'type' => 'node', - 'missing' => TRUE, - ); - } - $action->parameters = unserialize($action->parameters); - $action->label = $actions[$action->callback]['label']; - $action->configurable = $actions[$action->callback]['configurable']; - $action->behavior = isset($actions[$action->callback]['behavior']) ? $actions[$action->callback]['behavior'] : array(); - $action->type = $actions[$action->callback]['type']; - $action->missing = !empty($actions[$action->callback]['missing']); - - $flag_actions[$action->aid] = $action; - } - } - - // Filter actions to a specified flag. - if (isset($flag_name)) { - $specific_flag_actions = array(); - foreach ($flag_actions as $aid => $action) { - if ($action->flag == $flag_name) { - $specific_flag_actions[$aid] = $action; - } - } - return $specific_flag_actions; - } - - return $flag_actions; -} - -/** - * Insert a new flag action. - * - * @param $fid - * The flag object ID. - * @param $event - * The flag event, such as "flag" or "unflag". - * @param $threshold - * The flagging threshold at which this action will be executed. - * @param $repeat_threshold - * The number of additional flaggings after which the action will be repeated. - * @param $callback - * The action callback to be executed. - * @param $parameters - * The action parameters. - */ -function flag_actions_insert_action($fid, $event, $threshold, $repeat_threshold, $callback, $parameters) { - return db_insert('flag_actions') - ->fields(array( - 'fid' => $fid, - 'event' => $event, - 'threshold' => $threshold, - 'repeat_threshold' => $repeat_threshold, - 'callback' => $callback, - 'parameters' => serialize($parameters), - )) - ->execute(); -} - -/** - * Update an existing flag action. - * - * @param $aid - * The flag action ID to update. - * @param $event - * The flag event, such as "flag" or "unflag". - * @param $threshold - * The flagging threshold at which this action will be executed. - * @param $repeat_threshold - * The number of additional flaggings after which the action will be repeated. - * @param $parameters - * The action parameters. - */ -function flag_actions_update_action($aid, $event, $threshold, $repeat_threshold, $parameters) { - return db_update('flag_actions') - ->fields(array( - 'event' => $event, - 'threshold' => $threshold, - 'repeat_threshold' => $repeat_threshold, - 'parameters' => serialize($parameters), - )) - ->condition('aid', $aid) - ->execute(); -} - -/** - * Delete a flag action. - * - * @param $aid - * The flag action ID to delete. - */ -function flag_actions_delete_action($aid) { - return db_delete('flag_actions', array('return' => Database::RETURN_AFFECTED)) - ->condition('aid', $aid) - ->execute(); -} - -/** - * Perform flag actions. - */ -function flag_actions_do($event, $flag, $content_id, $account) { - $actions = flag_actions_get_actions($flag->name); - if (!$actions) { - return; - } - - $flag_action = $flag->get_flag_action($content_id); - $flag_action->action = $event; - $flag_action->count = $count = $flag->get_count($content_id); - $relevant_objects = $flag->get_relevant_action_objects($content_id); - $object_changed = FALSE; - foreach ($actions as $aid => $action) { - if ($action->event == 'flag') { - $at_threshold = ($count == $action->threshold); - $repeat = $action->repeat_threshold ? (($count > $action->threshold) && (($count - $action->threshold) % $action->repeat_threshold == 0)) : FALSE; - } - elseif ($action->event == 'unflag') { - $at_threshold = ($count == $action->threshold - 1); - $repeat = $action->repeat_threshold ? (($count < $action->threshold - 1) && (($count - $action->threshold - 1) % $action->repeat_threshold == 0)) : FALSE; - } - if (($at_threshold || $repeat) && $action->event == $event && !$action->missing) { - $context = $action->parameters; - $context['callback'] = $action->callback; - // We're setting 'hook' to something, to prevent PHP warnings by actions - // who read it. Maybe we should set it to nodeapi/comment/user, depending - // on the flag, because these three are among the only hooks some actions - // in system.module "know" to work with. - $context['hook'] = 'flag'; - $context['type'] = $action->type; - $context['account'] = $account; - $context['flag'] = $flag; - $context['flag-action'] = $flag_action; - // We add to the $context all the objects we know about: - $context = array_merge($relevant_objects, $context); - $callback = $action->callback; - - if (isset($relevant_objects[$action->type])) { - $callback($relevant_objects[$action->type], $context); - } - else { - // What object shall we send as last resort? Let's send a node, or - // the flag's object. - if (isset($relevant_objects['node'])) { - $callback($relevant_objects['node'], $context); - } - else { - $callback($relevant_objects[$flag->content_type], $context); - } - } - - if (is_array($action->behavior) && in_array('changes_property', $action->behavior)) { - $object_changed = TRUE; - } - } - } - - // Actions by default do not save elements unless the save action is - // explicitly added. We run it automatically upon flagging. - if ($object_changed) { - $save_action = $action->type . '_save_action'; - if (function_exists($save_action)) { - $save_action($relevant_objects[$action->type]); - } - } -} - -/** - * Menu callback for FLAG_ADMIN_PATH/actions. - */ -function flag_actions_page() { - $actions = flag_actions_get_actions(); - $add_action_form = drupal_get_form('flag_actions_add_form'); - - return theme('flag_actions_page', array('actions' => $actions, 'form' => $add_action_form)); -} - -/** - * Theme the list of actions currently in place for flags. - */ -function theme_flag_actions_page($variables) { - $actions = $variables['actions']; - $add_action_form = $variables['form']; - - $rows = array(); - foreach ($actions as $action) { - $flag = flag_get_flag($action->flag); - - // Build a sample string representing repeating actions. - if ($action->repeat_threshold) { - $repeat_count = 3; - $repeat_subtract = ($action->event == 'flag') ? 1 : -1; - $repeat_samples = array(); - for ($n = 1; $n < $repeat_count + 2; $n++) { - $sample = $action->threshold + (($n * $action->repeat_threshold) * $repeat_subtract); - if ($sample > 0) { - $repeat_samples[] = $sample; - } - } - if (count($repeat_samples) > $repeat_count) { - $repeat_samples[$repeat_count] = '…'; - } - $repeat_string = implode(', ', $repeat_samples); - } - else { - $repeat_string = '-'; - } - - $row = array(); - $row[] = $flag->get_title(); - $row[] = ($action->event == 'flag' ? '≥ ' : '< ') . $action->threshold; - $row[] = $repeat_string; - $row[] = empty($action->missing) ? $action->label : '
' . $action->label . '
'; - $row[] = l(t('edit'), FLAG_ADMIN_PATH . '/actions/configure/' . $action->aid); - $row[] = l(t('delete'), FLAG_ADMIN_PATH . '/actions/delete/' . $action->aid); - $rows[] = $row; - } - - if (empty($rows)) { - $rows[] = array(array('data' => t('Currently no flag actions. Use the Add new flag action form to add an action.'), 'colspan' => 6)); - } - - $header = array( - t('Flag'), - t('Threshold'), - t('Repeats'), - t('Action'), - array('data' => t('Operations'), 'colspan' => 2), - ); - - $output = ''; - $output .= theme('table', array('header' => $header, 'rows' => $rows)); - $output .= drupal_render($add_action_form); - return $output; -} - -/** - * Modified version of the Add action form that redirects back to the flag list. - */ -function flag_actions_add_form($form, &$form_state) { - $flags = flag_get_flags(); - $options = array(); - foreach ($flags as $flag) { - $options[$flag->name] = $flag->get_title(); - } - - if (empty($options)) { - $options[] = t('No flag available'); - } - - $form['flag'] = array( - '#type' => 'select', - '#options' => empty($options) ? array(t('No flag available')) : $options, - '#disabled' => empty($options), - '#title' => t('Select a flag'), - ); - - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Add action'), - ); - - return $form; -} - -function flag_actions_add_form_submit($form, &$form_state) { - if ($form_state['values']['flag']) { - $form_state['redirect'] = array(FLAG_ADMIN_PATH . '/actions/add/' . $form_state['values']['flag']); - } -} - -function theme_flag_actions_add_form($variables) { - $form = $variables['form']; - - $fieldset = array( - '#type' => 'fieldset', - '#title' => t('Add a new flag action'), - '#children' => '
'. drupal_render($form['flag']) . drupal_render($form['submit']) .'
', - '#parents' => array('add_action'), - '#attributes' => array(), - '#groups' => array('add_action' => array()), - ); - - return drupal_render($fieldset) . drupal_render_children($form); -} - -/** - * Generic configuration form for configuration of flag actions. - * - * @param $form_state - * The form state. - * @param $aid - * If editing an action, an action ID must be passed in. - * @param $flag_name - * If adding a new action to a flag, a flag name must be specified. - * - */ -function flag_actions_form($form, &$form_state, $aid = NULL, $flag_name = NULL) { - // This is a multistep form. Get the callback value if set and continue. - if (isset($form_state['storage']['callback'])) { - $callback = $form_state['storage']['callback']; - unset($form_state['storage']['callback']); - } - - if (isset($aid)) { - $action = flag_actions_get_action($aid); - $callback = $action->callback; - $flag = flag_get_flag($action->flag); - drupal_set_title(t('Edit the "@action" action for the @title flag', array('@action' => $action->label, '@title' => $flag->get_title()))); - } - elseif (isset($flag_name)) { - $flag = flag_get_flag($flag_name); - } - - if (empty($flag)) { - drupal_not_found(); - } - - $form['new'] = array( - '#type' => 'value', - '#value' => isset($callback) ? FALSE: TRUE, - ); - - if (!isset($callback)) { - drupal_set_title(t('Add an action to the @title flag', array('@title' => $flag->get_title()))); - - $actions = $flag->get_valid_actions(); - $options = array(); - foreach($actions as $key => $action) { - $options[$key] = $action['label']; - } - - $form['callback'] = array( - '#title' => t('Select an action'), - '#type' => 'select', - '#options' => $options, - ); - - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Continue'), - ); - - return $form; - } - elseif (!isset($action)) { - $actions = $flag->get_valid_actions(); - $action = (object)$actions[$callback]; - $action->parameters = array(); - $action->event = 'flag'; - $action->threshold = 10; - $action->repeat_threshold = 0; - drupal_set_title(t('Add "@action" action to the @title flag', array('@action' => $action->label, '@title' => $flag->get_title()))); - } - - $form['flag'] = array( - '#tree' => TRUE, - '#weight' => -9, - '#theme' => 'flag_actions_flag_form', - '#action' => $action, - '#flag' => $flag, - ); - - $form['flag']['flag'] = array( - '#type' => 'value', - '#value' => $flag, - ); - - $form['flag']['callback'] = array( - '#type' => 'value', - '#value' => $callback, - ); - - $form['flag']['aid'] = array( - '#type' => 'value', - '#value' => $aid, - ); - - $form['flag']['event'] = array( - '#type' => 'select', - '#options' => array( - 'flag' => t('reaches'), - 'unflag' => t('falls below'), - ), - '#default_value' => $action->event, - ); - - $form['flag']['threshold'] = array( - '#type' => 'textfield', - '#size' => 6, - '#maxlength' => 6, - '#default_value' => $action->threshold, - '#required' => TRUE, - ); - - $form['flag']['repeat_threshold'] = array( - '#type' => 'textfield', - '#size' => 6, - '#maxlength' => 6, - '#default_value' => $action->repeat_threshold, - ); - - if ($flag->global) { - $form['flag']['threshold']['#disabled'] = 1; - $form['flag']['threshold']['#value'] = 1; - $form['flag']['repeat_threshold']['#access'] = FALSE; - $form['flag']['repeat_threshold']['#value'] = 0; - } - - // Merge in the standard flag action form. - $action_form = $callback .'_form'; - $edit = array(); - if (function_exists($action_form)) { - $edit += $action->parameters; - $edit['actions_label'] = $action->label; - $edit['actions_type'] = $action->type; - $edit['actions_flag'] = $flag->name; - $additions = flag_actions_form_additions($action_form, $edit); - $form = array_merge($form, $additions); - } - - // Add a few customizations to existing flag actions. - $flag_actions_form = 'flag_actions_'. $callback .'_form'; - if (function_exists($flag_actions_form)) { - $flag_actions_form($form, $flag, $edit); - } - - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Submit'), - ); - - return $form; -} - -/** - * Execute an action form callback to retrieve form additions. - * - * This function prevents the form callback from modifying local variables. - */ -function flag_actions_form_additions($callback, $edit) { - return $callback($edit); -} - -/** - * Generic submit handler for validating flag actions. - */ -function flag_actions_form_validate($form, &$form_state) { - // Special validation handlers may be needed to save this form properly. - // Try to load the action's validation routine if needed. - if (isset($form_state['values']['flag']['callback'])) { - $callback = $form_state['values']['flag']['callback']; - $validate_function = $callback . '_validate'; - if (function_exists($validate_function)) { - $validate_function($form, $form_state); - } - } -} - -/** - * Generic submit handler for saving flag actions. - */ -function flag_actions_form_submit($form, &$form_state) { - // If simply gathering the callback, save it to form state storage and - // rebuild the form to gather the complete information. - if ($form_state['values']['new']) { - $form_state['storage']['callback'] = $form_state['values']['callback']; - $form_state['rebuild'] = TRUE; - return; - } - - $aid = $form_state['values']['flag']['aid']; - $flag = $form_state['values']['flag']['flag']; - $event = $form_state['values']['flag']['event']; - $threshold = $form_state['values']['flag']['threshold']; - $repeat_threshold = $form_state['values']['flag']['repeat_threshold']; - $callback = $form_state['values']['flag']['callback']; - - // Specialized forms may need to execute their own submit handlers on save. - $submit_function = $callback . '_submit'; - $parameters = function_exists($submit_function) ? $submit_function($form, $form_state) : array(); - - if (empty($aid)) { - $aid = flag_actions_insert_action($flag->fid, $event, $threshold, $repeat_threshold, $callback, $parameters); - $form_state['values']['flag']['aid'] = $aid; - $form_state['values']['flag']['is_new'] = TRUE; - } - else { - flag_actions_update_action($aid, $event, $threshold, $repeat_threshold, $parameters); - } - - $action = flag_actions_get_action($aid); - - drupal_set_message(t('The "@action" action for the @title flag has been saved.', array('@action' => $action->label, '@title' => $flag->get_title()))); - $form_state['redirect'] = FLAG_ADMIN_PATH . '/actions'; -} - -function theme_flag_actions_flag_form($variables) { - $form = $variables['form']; - - $event = drupal_render($form['event']); - $threshold = drupal_render($form['threshold']); - $repeat_threshold = drupal_render($form['repeat_threshold']); - $action = $form['#action']->label; - - $output = ''; - $output .= '
'; - $output .= t('Perform action when content !event !threshold flags', array('!event' => $event, '!threshold' => $threshold)); - if ($form['#flag']->global) { - $output .= ' ' . t('(global flags always have a threshold of 1)'); - } - $output .= '
'; - $output .= '
'; - if (!$form['#flag']->global) { - $output .= t('Repeat this action every !repeat_threshold additional flags after the threshold is reached', array('!repeat_threshold' => $repeat_threshold)); - } - $output .= '
'; - - $element = array( - '#title' => t('Flagging threshold'), - '#required' => TRUE, - ); - - return $output . drupal_render_children($form); -} - -function flag_actions_delete_form($form, &$form_state, $aid) { - $action = flag_actions_get_action($aid); - $flag = flag_get_flag($action->flag); - - $form['action'] = array( - '#type' => 'value', - '#value' => $action, - ); - - $form['flag'] = array( - '#type' => 'value', - '#value' => $flag, - ); - - $question = t('Delete the "@action" action for the @title flag?', array('@action' => $action->label, '@title' => $flag->get_title())); - $path = FLAG_ADMIN_PATH . '/actions'; - - return confirm_form($form, $question, $path, NULL, t('Delete')); -} - -function flag_actions_delete_form_submit(&$form, &$form_state) { - flag_actions_delete_action($form_state['values']['action']->aid); - drupal_set_message(t('The "@action" action for the @title flag has been deleted.', array('@action' => $form_state['values']['action']->label, '@title' => $form_state['values']['flag']->get_title()))); - $form_state['redirect'] = FLAG_ADMIN_PATH . '/actions'; -} - -/** - * Make modifications to the "Send e-mail" action form. - */ -function flag_actions_system_send_email_action_form(&$form, &$flag, $context) { - if (!isset($context['recipient'])) { - $form['recipient']['#default_value'] = '[site:mail]'; - } - - if (!isset($context['subject'])) { - $form['subject']['#default_value'] = t('Content Flagged @flag_title', array('@flag_title' => $flag->get_title())); - } - - if (!isset($context['message'])) { - $form['message']['#default_value'] = t("The @flag_content_type [flag-action:content-title] has been flagged [flag-action:count] times with the @flag_title flag.\n\nView this @flag_content_type at [flag-action:content-url].", array('@flag_content_type' => $flag->content_type, '@flag_title' => $flag->get_title())); - } - - $form['help'] = array( - '#type' => 'fieldset', - '#title' => t('Tokens'), - '#description' => t('The following tokens can be used in the recipient, subject, or message.'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - $form['help']['basic'] = array( - '#markup' => theme('flag_tokens_browser', array('types' => array('flag', 'flag-action'))), - ); - - $form['help']['tokens'] = array( - '#type' => 'fieldset', - '#title' => t('More tokens'), - '#description' => t("Depending on the type of the content being flagged, the following tokens can be used in the recipients, subject, or message. For example, if the content being flagged is a node, you can use any of the node tokens --but you can't use the comment tokens: they won't be recognized. Similarly, if the content being flagged is a user, you can use only the user tokens."), - '#value' => theme('flag_tokens_browser', array('types' => $flag->get_labels_token_types(), 'global_types' => FALSE)), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); -} diff --git a/flag_bookmark/config/install/flag.flag.bookmark.yml b/flag_bookmark/config/install/flag.flag.bookmark.yml new file mode 100644 index 0000000..6e16e55 --- /dev/null +++ b/flag_bookmark/config/install/flag.flag.bookmark.yml @@ -0,0 +1,33 @@ +id: bookmark +entity_type: node +label: Bookmark +types: + article: article + page: page +flag_short: 'Bookmark this' +flag_long: 'Add this post to your bookmarks' +flag_message: 'This post has been added to your bookmarks' +unflag_short: 'Remove bookmark' +unflag_long: 'Remove this post from your bookmarks' +unflag_message: 'This post has been removed from your bookmarks' +unflag_denied_text: '' +weight: 0 +status: true +langcode: en +dependencies: { } +roles: null +flag_type: flagtype_node +link_type: reload +flagTypeConfig: + show_in_links: + full: full + teaser: teaser + rss: 0 + search_index: 0 + search_result: 0 + show_as_field: 1 + show_on_form: 0 + show_contextual_link: 0 + i18n: null + access_author: '' +linkTypeConfig: { } diff --git a/flag_bookmark/config/install/views.view.flag_bookmark.yml b/flag_bookmark/config/install/views.view.flag_bookmark.yml new file mode 100644 index 0000000..7224fb8 --- /dev/null +++ b/flag_bookmark/config/install/views.view.flag_bookmark.yml @@ -0,0 +1,499 @@ +base_field: nid +base_table: node +core: 8.x +description: '' +status: true +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + provider: user + dependencies: { } + cache: + type: none + options: { } + provider: views + dependencies: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + slave: false + query_comment: false + query_tags: { } + provider: views + dependencies: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + provider: views + dependencies: { } + pager: + type: full + options: + items_per_page: 25 + offset: 0 + id: 0 + total_pages: null + tags: + previous: '‹ previous' + next: 'next ›' + first: '« first' + last: 'last »' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 20, 40, 60' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + quantity: 9 + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + title: title + created: created + changed: changed + created_1: created_1 + info: + title: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + created: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + created_1: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: '-1' + empty_table: false + row: + type: fields + fields: + type: + id: type + table: node_field_data + field: type + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - node + label: Type + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: '' + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + link_to_node: false + machine_name: '0' + plugin_id: node_type + provider: node + title: + id: title + table: node_field_data + field: title + provider: node + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_node: 1 + relationship: none + group_type: group + admin_label: '' + dependencies: { } + label: Title + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + name: + id: name + table: users + field: name + relationship: uid + group_type: group + admin_label: '' + dependencies: + module: + - user + label: Author + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: '' + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + link_to_user: true + overwrite_anonymous: false + anonymous_text: '' + format_username: true + plugin_id: user_name + provider: user + comment_count: + id: comment_count + table: comment_entity_statistics + field: comment_count + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + - views + label: Replies + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: '' + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '0' + hide_empty: false + empty_zero: true + hide_alter_empty: false + set_precision: false + precision: 0 + decimal: . + separator: ',' + format_plural: false + format_plural_singular: '1' + format_plural_plural: '@count' + prefix: '' + suffix: '' + plugin_id: numeric + provider: views + link_flag: + id: link_flag + table: flagging + field: link_flag + relationship: flag_content_rel + group_type: group + admin_label: '' + dependencies: + module: + - flag + - flag + label: Ops + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: '' + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + text: '' + plugin_id: flag_link + provider: flag + filters: + status: + value: true + table: node_field_data + field: status + provider: node + id: status + expose: + operator: '' + group: 1 + flagged: + id: flagged + table: flagging + field: flagged + relationship: flag_content_rel + group_type: group + admin_label: '' + dependencies: + module: + - flag + - flag + - flag + - flag + - flag + - flag + - flag + - flag + - flag + operator: '=' + value: '1' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: flag_filter + provider: flag + sorts: { } + title: 'My Bookmarks' + header: { } + footer: { } + empty: { } + relationships: + flag_content_rel: + id: flag_content_rel + table: node_field_data + field: flag_content_rel + relationship: none + group_type: group + admin_label: Flags + dependencies: + module: + - flag + - flag + required: false + flag: 'Stuff! custom' + user_scope: current + plugin_id: flag_relationship + provider: flag + uid: + id: uid + table: node_field_data + field: uid + relationship: none + group_type: group + admin_label: author + dependencies: + module: + - views + required: true + plugin_id: standard + provider: views + arguments: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + provider: views + display_options: + path: bookmarks + menu: + type: normal + title: 'My Bookmarks' + description: '' + name: main + weight: 0 + context: '0' +label: flag_bookmark +module: views +id: flag_bookmark +tag: '' +langcode: en +dependencies: + module: + - flag + - node + - user diff --git a/flag_bookmark/flag_bookmark.info.yml b/flag_bookmark/flag_bookmark.info.yml new file mode 100644 index 0000000..d5babcc --- /dev/null +++ b/flag_bookmark/flag_bookmark.info.yml @@ -0,0 +1,7 @@ +name: Flag Bookmark +description: Provides an example bookmark flag and supporting views. +core: 8.x +type: module +dependencies: + - flag +package: Flags diff --git a/flag_bookmark/flag_bookmark.module b/flag_bookmark/flag_bookmark.module new file mode 100644 index 0000000..626ccd8 --- /dev/null +++ b/flag_bookmark/flag_bookmark.module @@ -0,0 +1,9 @@ + array( - 'flag_flag' => array( - 'label' => t('Content has been flagged with any flag'), - ), - 'flag_unflag' => array( - 'label' => t('Content has been unflagged with any flag') - ), - ), - ); - - foreach (flag_get_flags() as $flag) { - $hooks['flag']['flag_flag_'. $flag->name]['label'] = t('A %type has been flagged with %name', array('%type' => $flag->content_type, '%name' => $flag->name)); - $hooks['flag']['flag_unflag_'. $flag->name]['label'] = t('A %type has been unflagged with %name', array('%type' => $flag->content_type, '%name' => $flag->name)); - } - - return $hooks; -} - -/** - * Implementation of hook_action_info(). - */ -function flag_action_info() { - return array( - 'flag_node_action' => array( - 'type' => 'node', - 'label' => t('Flag (or unflag) a node'), - 'configurable' => TRUE, - 'triggers' => array( - 'node_presave', 'node_insert', 'node_update', 'node_delete', 'node_view', - 'comment_insert', 'comment_update', 'comment_delete', 'comment_view', - ), - ), - 'flag_comment_action' => array( - 'type' => 'comment', - 'label' => t('Flag (or unflag) a comment'), - 'configurable' => TRUE, - 'triggers' => array( - 'comment_insert', 'comment_update', 'comment_delete', 'comment_view', - ), - ), - 'flag_user_action' => array( - 'type' => 'user', - 'label' => t('Flag (or unflag) a user'), - 'configurable' => TRUE, - 'triggers' => array( - 'user_insert', 'user_update', 'user_delete', 'user_login', 'user_logout', 'user_view', - ), - ), - ); -} - -/** - * Implementation of hook_action_info_alter(). - * - * Enable Flag actions on Node, Comment, and User hooks without - * the trigger_unlock.module. - */ -function flag_action_info_alter(&$actions) { - $node_flags = flag_get_flags('node'); - $comment_flags = flag_get_flags('comment'); - $user_flags = flag_get_flags('user'); - - foreach ($actions as $name => $action) { - if (strpos($name, 'node') === 0) { - $actions[$name]['triggers'][] = 'flag_flag'; - $actions[$name]['triggers'][] = 'flag_unflag'; - foreach ($node_flags as $flag) { - $actions[$name]['triggers'][] = 'flag_flag_' . $flag->name; - $actions[$name]['triggers'][] = 'flag_unflag_' . $flag->name; - } - } - if (strpos($name, 'comment') === 0) { - $actions[$name]['triggers'][] = 'flag_flag'; - $actions[$name]['triggers'][] = 'flag_unflag'; - foreach ($comment_flags as $flag) { - $actions[$name]['triggers'][] = 'flag_flag_' . $flag->name; - $actions[$name]['triggers'][] = 'flag_unflag_' . $flag->name; - } - } - if (strpos($name, 'user') === 0) { - $actions[$name]['triggers'][] = 'flag_flag'; - $actions[$name]['triggers'][] = 'flag_unflag'; - foreach ($user_flags as $flag) { - $actions[$name]['triggers'][] = 'flag_flag_' . $flag->name; - $actions[$name]['triggers'][] = 'flag_unflag_' . $flag->name; - } - } - } -} - -/** - * Implementation of a Drupal action. Flags a node. - * - * Note the first parameter is "object" because it may be a comment or a node. - */ -function flag_node_action(&$object, $context = array()) { - if ($flag = flag_get_flag($context['flag_action']['flag'])) { - $account = isset($context['account']) ? $context['account'] : $GLOBALS['user']; - $flag->flag($context['flag_action']['op'], $object->nid, $account, TRUE); - } -} - -/** - * Form for configuring the Flag node action. - */ -function flag_node_action_form($context = array()) { - return flag_action_form($context, 'node'); -} - -/** - * Submit function for the Flag node action form. - */ -function flag_node_action_submit($form, $form_state) { - return flag_action_submit($form, $form_state); -} - -/** - * Implementation of a Drupal action. Flags a comment. - */ -function flag_comment_action(&$comment, $context = array()) { - if ($flag = flag_get_flag($context['flag_action']['flag'])) { - $account = isset($context['account']) ? $context['account'] : $GLOBALS['user']; - $flag->flag($context['flag_action']['op'], $comment->cid, $account, TRUE); - } -} - -/** - * Form for configuring the Flag comment action. - */ -function flag_comment_action_form($context) { - return flag_action_form($context, 'comment'); -} - -/** - * Submit function for the Flag comment action form. - */ -function flag_comment_action_submit($form, $form_state) { - return flag_action_submit($form, $form_state); -} - -/** - * Implementation of a Drupal action. Flags a user. - */ -function flag_user_action(&$user, $context = array()) { - if ($flag = flag_get_flag($context['flag_action']['flag'])) { - $account = isset($context['account']) ? $context['account'] : $GLOBALS['user']; - $flag->flag($context['flag_action']['op'], $user->uid, $account, TRUE); - } -} - -/** - * Form for configuring the Flag user action. - */ -function flag_user_action_form($context) { - return flag_action_form($context, 'user'); -} - -/** - * Submit function for the Flag user action form. - */ -function flag_user_action_submit($form, $form_state) { - return flag_action_submit($form, $form_state); -} - -/** - * Generic form for configuring Flag actions. - * - * @param $context - * The current action context. - * @param $content_type - * The content type applicable to this action, such as "node" or "comment". - */ -function flag_action_form($context, $content_type) { - $form = array(); - - $flags = flag_get_flags($content_type); - // If this is a flag_action action, do not allow the triggering flag. - if (isset($context['actions_flag'])) { - unset($flags[$context['actions_flag']]); - } - $options = drupal_map_assoc(array_keys($flags)); - - $form['flag_action']['#tree'] = TRUE; - $form['flag_action']['warning'] = array( - '#markup' => '
' . t("Note when setting a flag through actions, the selected flag will be flagged regardless of the user's permissions.") . '
', - ); - $form['flag_action']['flag'] = array( - '#title' => t('Flag to affect'), - '#type' => 'radios', - '#options' => $options, - '#required' => TRUE, - '#description' => t('When this action is fired, which flag should be flagged (or unflagged)?'), - '#default_value' => isset($context['flag_action']['flag']) ? $context['flag_action']['flag'] : reset($options), - ); - - $form['flag_action']['op'] = array( - '#title' => t('Flag operation'), - '#type' => 'radios', - '#options' => array('flag' => t('Flag'), 'unflag' => t('Unflag')), - '#description' => t('When this action is fired, which operation should be performed on the flag?'), - '#default_value' => isset($context['flag_action']['op']) ? $context['flag_action']['op'] : 'flag', - ); - - if (empty($options)) { - $error = t('There are no available %type flags. Before you can create an action of this type, you need to create a %type flag.', array('%type' => $content_type, '!url' => url(FLAG_ADMIN_PATH . '/add'))); - $form['flag_action']['flag']['#type'] = 'item'; - $form['flag_action']['flag']['#markup'] = $error; - $form['flag_action']['flag']['#element_validate'][] = 'flag_action_validate_flag'; - $form['flag_action']['flag']['#flag_error'] = $error; - } - - return $form; -} - -/** - * Generic validation handler for validating Flag action configuration. - */ -function flag_action_validate_flag($element) { - if (isset($element['#flag_error'])) { - form_error($element, $element['#flag_error']); - } -} - -/** - * Generic submission handler for saving Flag action configuration. - */ -function flag_action_submit($form, $form_state) { - return array( - 'flag_action' => $form_state['values']['flag_action'], - ); -} diff --git a/includes/flag.admin.inc b/includes/flag.admin.inc deleted file mode 100644 index e44224b..0000000 --- a/includes/flag.admin.inc +++ /dev/null @@ -1,569 +0,0 @@ - $flags, 'default_flags' => $default_flags)); -} - -/** - * Theme the output for the main flag administration page. - */ -function theme_flag_admin_page($variables) { - $flags = $variables['flags']; - $default_flags = $variables['default_flags']; - $output = ''; - - // Build out the list of normal, database flags. - foreach ($flags as $flag) { - $ops = array( - 'flags_edit' => array('title' => t('edit'), 'href' => $flag->admin_path('edit')), - 'flags_delete' => array('title' => t('delete'), 'href' => $flag->admin_path('delete')), - 'flags_export' => array('title' => t('export'), 'href' => $flag->admin_path('export')), - ); - - $roles = array_flip(array_intersect(array_flip(user_roles()), $flag->roles['flag'])); - $rows[] = array( - $flag->name, - $flag->content_type, - empty($flag->roles['flag']) ? '' . t('No roles') . '' : implode(', ', $roles), - $flag->types ? implode(', ', $flag->types) : '-', - $flag->global ? t('Yes') : t('No'), - theme('links', array('links' => $ops)), - ); - } - if (!$flags) { - $rows[] = array( - array('data' => t('No flags are currently defined.'), 'colspan' => 6), - ); - } - - $header = array(t('Flag'), t('Flag type'), t('Roles'), t('Node types'), t('Global?'), t('Operations')); - $output .= theme('table', array('header' => $header, 'rows' => $rows)); - - // Build a list of disabled, module-based flags. - $rows = array(); - foreach ($default_flags as $name => $flag) { - if (!isset($flags[$name])) { - $ops = array(); - if (!$flag->is_compatible()) { - $flag_updates_needed = TRUE; - $ops['flags_update'] = array('title' => '' . t('update code') . '', 'href' => $flag->admin_path('update'), 'html' => TRUE); - } - else { - $ops['flags_enable'] = array('title' => t('enable'), 'href' => $flag->admin_path('edit')); - } - // $flag->roles['flag'] not exist on older flags. - $roles = array_flip(array_intersect(array_flip(user_roles()), !empty($flag->roles['flag']) ? $flag->roles['flag'] : array())); - $rows[] = array( - $flag->name, - $flag->module, - $flag->content_type, - theme('links', array('links' => $ops)), - ); - } - } - - if (isset($flag_updates_needed)) { - drupal_set_message(t('Some flags provided by modules need to be updated to a new format before they can be used with this version of Flag. See the disabled flags for a list of flags that need updating.'), 'warning'); - } - - if (!empty($rows)) { - $header = array(t('Disabled Flags'), t('Module'), t('Flag type'), t('Operations')); - $output .= theme('table', array('header' => $header, 'rows' => $rows)); - } - - if (!module_exists('views')) { - $output .= '

' . t('The Views module is not installed, or not enabled. It is recommended that you install the Views module to be able to easily produce lists of flagged content.', array('@views-url' => url('http://drupal.org/project/views'))) . '

'; - } - else { - $output .= '

'; - $output .= t('Lists of flagged content can be displayed using views. You can configure these in the Views administration section.', array('@views-url' => url('admin/structure/views'))); - if (flag_get_flag('bookmarks')) { - $output .= ' ' . t('Flag module automatically provides a few default views for the bookmarks flag. You can use these as templates by cloning these views and then customizing as desired.', array('@views-url' => url('admin/structure/views', array('query' => array('tag' => 'flag'))))); - } - $output .= ' ' . t('The Flag module handbook contains extensive documentation on creating customized views using flags.', array('@flag-handbook-url' => 'http://drupal.org/handbook/modules/flag', '@customize-url' => 'http://drupal.org/node/296954')); - $output .= '

'; - } - - if (!module_exists('flag_actions')) { - $output .= '

' . t('Flagging an item may trigger actions. However, you don\'t have the Flag actions module enabled, so you won\'t be able to enjoy this feature.', array('@actions-url' => url(FLAG_ADMIN_PATH . '/actions'), '@modules-url' => url('admin/modules'))) . '

'; - } - else { - $output .= '

' . t('Flagging an item may trigger actions.', array('@actions-url' => url(FLAG_ADMIN_PATH . '/actions'))) . '

'; - } - - if (!module_exists('rules')) { - $output .= '

' . t('Flagging an item may trigger rules. However, you don\'t have the Rules module enabled, so you won\'t be able to enjoy this feature. The Rules module is a more extensive solution than Flag actions.', array('@rules-url' => url('http://drupal.org/node/407070'))) . '

'; - } - else { - $output .= '

' . t('Flagging an item may trigger rules.', array('@rules-url' => url('admin/config/workflow/rules'))) . '

'; - } - - $output .= '

' . t('To learn about the various ways to use flags, please check out the Flag module handbook.', array('@handbook-url' => 'http://drupal.org/handbook/modules/flag')) . '

'; - - return $output; -} - -/** - * Menu callback for adding a new flag. - */ -function flag_add_page($type = NULL, $name = NULL) { - drupal_set_title(t('Add new flag')); - if (isset($type) && isset($name)) { - $flag = flag_flag::factory_by_content_type($type); - $flag->name = $name; - return drupal_get_form('flag_form', $flag); - } - else { - return drupal_get_form('flag_add_form'); - } -} - -/** - * Present a form for creating a new flag, setting the type of flag. - */ -function flag_add_form($form, &$form_state) { - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Flag name'), - '#description' => t('The machine-name for this flag. It may be up to 32 characters long and may only contain lowercase letters, underscores, and numbers. It will be used in URLs and in all API calls.'), - '#maxlength' => 32, - '#required' => TRUE, - ); - - $types = array(); - foreach (flag_fetch_definition() as $type => $info) { - $types[$type] = $info['title'] . '
' . $info['description'] . '
'; - } - - $form['type'] = array( - '#type' => 'radios', - '#title' => t('Flag type'), - '#default_value' => 'node', - '#description' => t('The type of content this flag will affect. An individual flag can only affect one type of content. This cannot be changed once the flag is created.'), - '#required' => TRUE, - '#options' => $types, - ); - - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Submit'), - ); - - return $form; -} - -function flag_add_form_validate($form, &$form_state) { - $flag = flag_flag::factory_by_content_type($form_state['values']['type']); - $flag->name = $form_state['values']['name']; - $errors = $flag->validate_name(); - foreach ($errors as $field => $field_errors) { - foreach ($field_errors as $error) { - form_set_error($field, $error['message']); - } - } -} - -function flag_add_form_submit($form, &$form_state) { - $form_state['redirect'] = FLAG_ADMIN_PATH . '/add/' . $form_state['values']['type'] . '/' . $form_state['values']['name']; -} - -/** - * Add/Edit flag page. - */ -function flag_form($form, &$form_state, $flag) { - $form['#flag'] = $flag; - - $form['name'] = array( - '#type' => 'textfield', - '#title' => t('Name'), - '#default_value' => $flag->name, - '#description' => t('The machine-name for this flag. It may be up to 32 characters long and may only contain lowercase letters, underscores, and numbers. It will be used in URLs and in all API calls.'), - '#maxlength' => 32, - '#required' => TRUE, - '#access' => empty($flag->locked['name']), - '#weight' => -3, - ); - - if (!empty($flag->fid)) { - $form['name']['#description'] .= ' '. t('Change this value only with great care.') .''; - } - - $form['title'] = array( - '#type' => 'textfield', - '#title' => t('Title'), - '#default_value' => $flag->title, - '#description' => t('A short, descriptive title for this flag. It will be used in administrative interfaces to refer to this flag, and in page titles and menu items of some views this module provides (theses are customizable, though). Some examples could be Bookmarks, Favorites, or Offensive.', array('@insite-views-url' => url('admin/structure/views'))), - '#maxlength' => 255, - '#required' => TRUE, - '#access' => empty($flag->locked['title']), - '#weight' => -2, - ); - - $form['global'] = array( - '#type' => 'checkbox', - '#title' => t('Global flag'), - '#default_value' => $flag->global, - '#description' => t('If checked, flag is considered "global" and each node is either flagged or not. If unchecked, each user has individual flags on content.'), - '#access' => empty($flag->locked['global']), - '#weight' => -1, - ); - - $form['messages'] = array( - '#type' => 'fieldset', - '#title' => t('Messages'), - ); - - $form['messages']['flag_short'] = array( - '#type' => 'textfield', - '#title' => t('Flag link text'), - '#default_value' => $flag->flag_short, - '#description' => t('The text for the "flag this" link for this flag.'), - '#required' => TRUE, - '#access' => empty($flag->locked['flag_short']), - ); - - $form['messages']['flag_long'] = array( - '#type' => 'textfield', - '#title' => t('Flag link description'), - '#default_value' => $flag->flag_long, - '#description' => t('The description of the "flag this" link. Usually displayed on mouseover.'), - '#access' => empty($flag->locked['flag_long']), - ); - - $form['messages']['flag_message'] = array( - '#type' => 'textfield', - '#title' => t('Flagged message'), - '#default_value' => $flag->flag_message, - '#description' => t('Message displayed after flagging content. If JavaScript is enabled, it will be displayed below the link. If not, it will be displayed in the message area.'), - '#access' => empty($flag->locked['flag_message']), - ); - - $form['messages']['unflag_short'] = array( - '#type' => 'textfield', - '#title' => t('Unflag link text'), - '#default_value' => $flag->unflag_short, - '#description' => t('The text for the "unflag this" link for this flag.'), - '#required' => TRUE, - '#access' => empty($flag->locked['unflag_short']), - ); - - $form['messages']['unflag_long'] = array( - '#type' => 'textfield', - '#title' => t('Unflag link description'), - '#default_value' => $flag->unflag_long, - '#description' => t('The description of the "unflag this" link. Usually displayed on mouseover.'), - '#access' => empty($flag->locked['unflag_long']), - ); - - $form['messages']['unflag_message'] = array( - '#type' => 'textfield', - '#title' => t('Unflagged message'), - '#default_value' => $flag->unflag_message, - '#description' => t('Message displayed after content has been unflagged. If JavaScript is enabled, it will be displayed below the link. If not, it will be displayed in the message area.'), - '#access' => empty($flag->locked['unflag_message']), - ); - - $form['messages']['tokens_help'] = array( - '#title' => t('Token replacement'), - '#type' => 'fieldset', - '#description' => - '

' . t('The above six texts may contain any of the tokens listed below. For example, "Flag link text" could be entered as:') . '

' . - theme('item_list', array('items' => array( - t('Add <em>[node:title]</em> to your favorites'), - t('Add this [node:type] to your favorites'), - t('Vote for this proposal ([node:flag-vote-count] people have already done so)'), - ), 'attributes' => array('class' => 'token-examples'))) . - '

' . t('These tokens will be replaced with the appropriate fields from the node (or user, or comment).') . '

' . - theme('flag_tokens_browser', array('types' => $flag->get_labels_token_types())), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - ); - - $form['access'] = array( - '#type' => 'fieldset', - '#title' => t('Flag access'), - '#tree' => FALSE, - '#weight' => 10, - ); - - $options = array(); - $node_types = node_type_get_types(); - foreach ($node_types as $node_type) { - $options[$node_type->type] = check_plain($node_type->name); - } - $form['access']['types'] = array( - '#type' => 'checkboxes', - '#title' => t('Flaggable content'), - '#options' => $options, - '#default_value' => $flag->types, - '#description' => t('Check any node types that this flag may be used on. You must check at least one node type.'), - '#required' => TRUE, - '#weight' => 10, - '#access' => empty($flag->locked['types']), - ); - - // Disabled access breaks checkboxes unless #value is hard coded. - if (!empty($flag->locked['types'])) { - $form['access']['types']['#value'] = $flag->types; - } - - $form['access']['roles'] = array( - '#title' => t('Roles that may use this flag'), - '#access' => empty($flag->locked['roles']), - '#description' => t('Users may only unflag content if they have access to flag the content initially. Checking authenticated user will allow access for all logged-in users.'), - '#theme' => 'flag_form_roles', - '#theme_wrappers' => array('form_element'), - '#weight' => -2, - '#attached' => array( - 'js' => array(drupal_get_path('module', 'flag') . '/theme/flag-admin.js'), - 'css' => array(drupal_get_path('module', 'flag') . '/theme/flag-admin.css'), - ), - ); - if (module_exists('session_api')) { - $form['access']['roles']['#description'] .= ' ' . t('Support for anonymous users is being provided by Session API.'); - } - else { - $form['access']['roles']['#description'] .= ' ' . t('Anonymous users may flag content if the Session API module is installed.'); - } - - $form['access']['roles']['flag'] = array( - '#type' => 'checkboxes', - '#options' => user_roles(!module_exists('session_api')), - '#default_value' => $flag->roles['flag'], - '#parents' => array('roles', 'flag'), - ); - $form['access']['roles']['unflag'] = array( - '#type' => 'checkboxes', - '#options' => user_roles(!module_exists('session_api')), - '#default_value' => $flag->roles['unflag'], - '#parents' => array('roles', 'unflag'), - ); - - // Disabled access breaks checkboxes unless #value is hard coded. - if (!empty($flag->locked['roles'])) { - $form['access']['roles']['#type'] = 'value'; - $form['access']['roles']['#value'] = $flag->roles; - } - - $form['access']['unflag_denied_text'] = array( - '#type' => 'textfield', - '#title' => t('Unflag not allowed text'), - '#default_value' => $flag->unflag_denied_text, - '#description' => t('If a user is allowed to flag but not unflag, this text will be displayed after flagging. Often this is the past-tense of the link text, such as "flagged".'), - '#access' => empty($flag->locked['unflag_denied_text']), - '#weight' => -1, - ); - - $form['display'] = array( - '#type' => 'fieldset', - '#title' => t('Display options'), - '#description' => t('Flags are usually controlled through links that allow users to toggle their behavior. You can choose how users interact with flags by changing options here. It is legitimate to have none of the following checkboxes ticked, if, for some reason, you wish to place the the links on the page yourself.', array('@placement-url' => 'http://drupal.org/node/295383')), - '#tree' => FALSE, - '#weight' => 20, - ); - - $form['display']['link_type'] = array( - '#type' => 'radios', - '#title' => t('Link type'), - '#options' => _flag_link_type_options(), - '#option_descriptions' => _flag_link_type_descriptions(), - '#flag_link_fields' => _flag_link_type_fields(), - '#after_build' => array('flag_expand_link_option', 'flag_check_link_types'), - '#default_value' => $flag->link_type, - '#weight' => 2, - '#access' => empty($flag->locked['link_type']), - ); - - $form['display']['link_options_intro'] = array( - '#markup' => '', - '#weight' => 20, - ); - - $form['display']['link_options_confirm'] = array( - '#type' => 'fieldset', - '#title' => t('Options for the "Confirmation form" link type'), - // Any "link type" provider module must put its settings fields inside - // a fieldset whose HTML ID is link-options-LINKTYPE, where LINKTYPE is - // the machine-name of the link type. This is necessary for the - // radiobutton's JavaScript dependency feature to work. - '#id' => 'link-options-confirm', - '#weight' => 21, - ); - - $form['display']['link_options_confirm']['flag_confirmation'] = array( - '#type' => 'textfield', - '#title' => t('Flag confirmation message'), - '#default_value' => isset($flag->flag_confirmation) ? $flag->flag_confirmation : '', - '#description' => t('Message displayed if the user has clicked the "flag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to flag this content?"'), - '#access' => empty($flag->locked['flag_confirmation']), - ); - - $form['display']['link_options_confirm']['unflag_confirmation'] = array( - '#type' => 'textfield', - '#title' => t('Unflag confirmation message'), - '#default_value' => isset($flag->unflag_confirmation) ? $flag->unflag_confirmation : '', - '#description' => t('Message displayed if the user has clicked the "unflag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to unflag this content?"'), - '#access' => empty($flag->locked['unflag_confirmation']), - ); - - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Submit'), - // We put this button on the form before calling $flag->options_form() - // to give the flag handler a chance to remove it (e.g. flag_broken). - '#weight' => 999, - ); - - $flag->options_form($form); - - return $form; -} - -/** - * Add/Edit flag form validate. - */ -function flag_form_validate($form, &$form_state) { - $form_values = $form_state['values']; - - if ($form_values['link_type'] == 'confirm') { - if (empty($form_values['flag_confirmation'])) { - form_set_error('flag_confirmation', t('A flag confirmation message is required when using the confirmation link type.')); - } - if (empty($form_values['unflag_confirmation'])) { - form_set_error('unflag_confirmation', t('An unflag confirmation message is required when using the confirmation link type.')); - } - } - - - $flag = $form['#flag']; - $flag->form_input($form_values); - $errors = $flag->validate(); - foreach ($errors as $field => $field_errors) { - foreach ($field_errors as $error) { - form_set_error($field, $error['message']); - } - } -} - -/** - * Add/Edit flag form submit. - */ -function flag_form_submit($form, &$form_state) { - $flag = $form['#flag']; - $flag->form_input($form_state['values']); - $flag->save(); - $flag->enable(); - drupal_set_message(t('Flag @name has been saved.', array('@name' => $flag->get_title()))); - _flag_clear_cache(); - $form_state['redirect'] = FLAG_ADMIN_PATH; -} - -/** - * Output the access options for roles in a table. - */ -function theme_flag_form_roles($variables) { - $element = $variables['element']; - - $header = array( - array('class' => array('checkbox'), 'data' => t('Flag')), - array('class' => array('checkbox'), 'data' => t('Unflag')), - t('Role'), - ); - $rows = array(); - foreach (element_children($element['flag']) as $role) { - $row = array(); - $role_name = $element['flag'][$role]['#title']; - unset($element['flag'][$role]['#title']); - unset($element['unflag'][$role]['#title']); - $element['flag'][$role]['#attributes']['class'] = array('flag-access'); - $element['unflag'][$role]['#attributes']['class'] = array('unflag-access'); - $row[] = array('class' => array('checkbox'), 'data' => drupal_render($element['flag'][$role])); - $row[] = array('class' => array('checkbox'), 'data' => drupal_render($element['unflag'][$role])); - $row[] = $role_name; - $rows[] = $row; - } - - return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('flag-admin-table'), 'id' => 'flag-roles'))); -} - -/** - * Delete flag page. - */ -function flag_delete_confirm($form, &$form_state, $flag) { - $form['fid'] = array('#type' => 'value', '#value' => $flag->fid); - - return confirm_form($form, - t('Are you sure you want to delete %title?', array('%title' => $flag->get_title())), - !empty($_GET['destination']) ? $_GET['destination'] : FLAG_ADMIN_PATH, - isset($flag->module) ? t('This flag is provided by the %module module. It will loose any customizations and be disabled.', array('%module' => $flag->module)) : t('This action cannot be undone.'), - t('Delete'), t('Cancel') - ); -} - -function flag_delete_confirm_submit($form, &$form_state) { - $flag = flag_get_flag(NULL, $form_state['values']['fid']); - if ($form_state['values']['confirm']) { - $flag->delete(); - $flag->disable(); - _flag_clear_cache(); - } - drupal_set_message(t('Flag @name has been deleted.', array('@name' => $flag->get_title()))); - $form_state['redirect'] = FLAG_ADMIN_PATH; -} - -/** - * FormAPI after_build function to add descriptions to radio buttons. - */ -function flag_expand_link_option($element) { - $element['#attached'] = array( - 'js' => array(drupal_get_path('module', 'flag') . '/theme/flag-admin.js'), - ); - - foreach (element_children($element) as $key) { - // Add a description to the link option. - if (isset($element['#option_descriptions'][$key])) { - $element[$key]['#description'] = $element['#option_descriptions'][$key]; - } - } - $element['#attributes']['class'] = array('flag-link-options'); - - return $element; -} - -/** - * FormAPI after_build function to check that the link type exists. - */ -function flag_check_link_types($element) { - $link_types = flag_get_link_types(); - if (!isset($link_types[$element['#value']])) { - drupal_set_message(t('This flag uses a link type of %type, which does not exist.', array('%type' => $element['#value'])), 'error'); - } - return $element; -} - -/** - * Clears various caches when a flag is modified. - */ -function _flag_clear_cache() { - if (module_exists('views')) { - views_invalidate_cache(); - } - // Reset our flags cache, thereby making the following code aware of the - // modifications. - flag_get_flags(NULL, NULL, NULL, TRUE); - // The title of a flag may appear in the menu (indirectly, via our "default - // views"), so we need to clear the menu cache. This call also clears the - // page cache, which is desirable too because the flag labels may have - // changed. - menu_rebuild(); -} diff --git a/includes/flag.export.inc b/includes/flag.export.inc deleted file mode 100644 index 26ffc0e..0000000 --- a/includes/flag.export.inc +++ /dev/null @@ -1,245 +0,0 @@ -is_compatible()) { - drupal_set_message(t('Could not export flag %flag-name: Your flag was created by a different version of the Flag module than is now being used.', array('%flag-name' => $flag->name)), 'error'); - continue; - } - - $flag->api_version = FLAG_API_VERSION; - $new_flag = (array) $flag; - - if (!empty($module)) { - // Even though Flag adds the module name itself later, we add the module - // name here for reference by other modules (such as Features). - $new_flag['module'] = $module; - // Lock the flag name, as is normally desired by modules using - // hook_flag_default_flags(), and needed by Features. - $new_flag['locked'] = array('name'); - } - // Allow other modules to change the exported flag. - drupal_alter('flag_export', $new_flag); - - // Remove the flag ID. - unset($new_flag['fid']); - // The name is emitted as the key for the array. - unset($new_flag['name']); - - $output .= $indent . '// Exported flag: "'. check_plain($flag->get_title()) . '"' . ".\n"; - $output .= $indent . '$flags[\'' . $flag->name . '\'] = ' . (function_exists('features_var_export') ? features_var_export($new_flag, $indent) : var_export($new_flag, TRUE)) . ";\n"; - } - $output .= $indent . 'return $flags;'; - return $output; -} - -/** - * Form to import a flag. - */ -function flag_import_form() { - $form = array(); - - $form['import'] = array( - '#title' => t('Flag import code'), - '#type' => 'textarea', - '#default_value' => '', - '#rows' => 15, - '#required' => TRUE, - '#description' => t('Paste the code from a flag export here to import it into you site. Flags imported with the same name will update existing flags. Flags with a new name will be created.', array('@export-url' => url(FLAG_ADMIN_PATH . '/export'))), - ); - $form['submit'] = array( - '#value' => t('Import'), - '#type' => 'submit', - ); - - return $form; -} - -/** - * Validate handler; Import a flag. - */ -function flag_import_form_validate($form, &$form_state) { - $flags = array(); - ob_start(); - eval($form_state['values']['import']); - ob_end_clean(); - - if (!isset($flags) || !is_array($flags)) { - form_set_error('import', t('A valid list of flags could be found in the import code.')); - return; - } - - // Create the flag object. - foreach ($flags as $flag_name => $flag_info) { - // Backward compatibility: old exported flags have their names in $flag_info - // instead, so we use the += operator to not overwrite it. - $flag_info += array( - 'name' => $flag_name, - ); - $new_flag = flag_flag::factory_by_array($flag_info); - - // Give new flags with the same name a matching FID, which tells Flag to - // update the existing flag, rather than creating a new one. - if ($existing_flag = flag_get_flag($new_flag->name)) { - $new_flag->fid = $existing_flag->fid; - } - - if ($errors = $new_flag->validate()) { - $message = t('The import of the %flag flag failed because the following errors were encountered during the import:', array('%flag' => $new_flag->name)); - $message_errors = array(); - foreach ($errors as $field => $field_errors) { - foreach ($field_errors as $error) { - $message_errors[] = $error['message']; - } - } - form_set_error('import', $message . theme('item_list', array('items' => $message_errors))); - } - else { - // Save the new flag for the submit handler. - $form_state['flags'][] = $new_flag; - } - } -} - -/** - * Submit handler; Import a flag. - */ -function flag_import_form_submit($form, &$form_state) { - module_load_include('inc', 'flag', 'includes/flag.admin'); - - foreach ($form_state['flags'] as $flag) { - $flag->save(); - if (!empty($flag->status)) { - $flag->enable(); - } - if ($flag->is_new) { - drupal_set_message(t('Flag @name has been imported.', array('@name' => $flag->name))); - } - else { - drupal_set_message(t('Flag @name has been updated.', array('@name' => $flag->name))); - } - } - _flag_clear_cache(); - - $form_state['redirect'] = FLAG_ADMIN_PATH; -} - -/** - * Export a flag and display it in a form. - */ -function flag_export_form($form, &$form_state, $flag = NULL) { - // If we were passed a flag, use it as the list of flags to export. - if ($flag) { - $flags = array($flag); - } - - // Display a list of flags to export. - if (!isset($flags)) { - if (isset($form_state['values']['flags'])) { - $flags = array(); - foreach ($form_state['values']['flags'] as $flag_name) { - if ($flag_name && $flag = flag_get_flag($flag_name)) { - $flags[] = $flag; - } - } - } - else { - $form['flags'] = array( - '#type' => 'checkboxes', - '#title' => t('Flags to export'), - '#options' => drupal_map_assoc(array_keys(flag_get_flags())), - '#description' => t('Exporting your flags is useful for moving flags from one site to another, or when including your flag definitions in a module.'), - ); - $form['submit'] = array( - '#type' => 'submit', - '#value' => t('Export'), - ); - } - } - - if (isset($flags)) { - $code = flag_export_flags($flags); - - // Link to the Features page if module is present, otherwise link to the - // Drupal project page. - $features_link = module_exists('features') ? url('admin/build/features') : url('http://drupal.org/project/features'); - - $form['export'] = array( - '#type' => 'textarea', - '#title' => t('Flag exports'), - '#description' => t('Use the exported code to later import it. Exports can be included in modules using hook_flag_default_flags() or using the Features module.', array('@import-flag' => url(FLAG_ADMIN_PATH . '/import'), '@features-url' => $features_link)), - '#value' => $code, - '#rows' => 15, - ); - } - - return $form; -} - -/** - * Submit handler; Rebuild the export form after the list of flags has been set. - */ -function flag_export_form_submit($form, &$form_state) { - $form_state['rebuild'] = TRUE; -} - -/** - * Page for displaying an upgrade message and export form for Flag 1.x flags. - */ -function flag_update_page($flag) { - if ($flag->is_compatible()) { - drupal_set_message(t('The flag %name is already up-to-date with the latest Flag API and does not need upgrading.')); - drupal_goto(FLAG_ADMIN_PATH); - } - - drupal_set_message(t('The flag %name is currently using the Flag API version @version, which is not compatible with the current version of Flag. You can upgrade this flag by pasting the below code into @module_flag_default_flags() function in the @module.module file.', array('%name' => $flag->name, '@version' => $flag->api_version, '@module' => $flag->module)), 'warning'); - - flag_update_export($flag); - - return drupal_get_form('flag_export_form', $flag); -} - -/** - * Update a flag before export. - * - * @param $flag - * The flag object passed by reference. - */ -function flag_update_export(&$flag) { - // Update differences. - if (empty($flag->api_version) || $flag->api_version == 1) { - if (isset($flag->roles) && !isset($flag->roles['flag'])) { - $flag->roles = array( - 'flag' => $flag->roles, - 'unflag' => $flag->roles, - ); - } - $flag->api_version = FLAG_API_VERSION; - } -} diff --git a/includes/flag.features.inc b/includes/flag.features.inc deleted file mode 100644 index eff1081..0000000 --- a/includes/flag.features.inc +++ /dev/null @@ -1,97 +0,0 @@ - $flag) { - // Get the module that provided the flag definition. - if ($flag = flag_load($flag, TRUE)) { - $module = $modules[$flag->content_type]; - $export['dependencies'][$module] = $module; - $export['features']['flag'][$flag->name] = $flag->name; - } - } - - return $pipe; -} - -/** - * Implementation of hook_features_export_options(). - */ -function flag_features_export_options() { - $options = array(); - // Get all flags, including disabled defaults. - $flags = flag_get_flags() + flag_get_default_flags(TRUE); - foreach ($flags as $name => $flag) { - $options[$name] = ucfirst(check_plain($flag->content_type)) .': '. check_plain($flag->title); - } - return $options; -} - -/** - * Implementation of hook_features_export_render(). - */ -function flag_features_export_render($module, $data) { - module_load_include('inc', 'flag', '/includes/flag.export'); - $code = flag_export_flags($data, $module, ' '); - return array('flag_default_flags' => $code); -} - -/** - * Implementation of hook_features_revert(). - * - * @param $module - * The name of module for which to revert content. - */ -function flag_features_revert($module = NULL) { - // Get default flags from features. - if (module_hook($module, 'flag_default_flags')) { - module_load_include('inc', 'flag', '/includes/flag.admin'); - $default_flags = module_invoke($module, 'flag_default_flags'); - - // Revert flags that are defined in code. - foreach ($default_flags as $flag_name => $flag_info) { - if (is_numeric($flag_name)) { - // Backward compatibility. - $flag_name = $flag_info['name']; - } - $flag = flag_load($flag_name, TRUE); - if ($flag && $flag->revert() === FALSE) { - drupal_set_message(t('Could not revert flag %flag-name to the state described in your code: Your flag was created by a different version of the Flag module than is now being used.', array('%flag-name' => $flag->name)), 'error'); - } - } - _flag_clear_cache(); - } - else { - drupal_set_message(t('Could not load default flag.'), 'error'); - return FALSE; - } - return TRUE; -} - -/** - * Helper function; Retrieve the providing modules defining the flags. - */ -function flag_features_providing_module() { - $modules = array(); - $hook = 'flag_definitions'; - foreach (module_implements($hook) as $module) { - foreach (module_invoke($module, $hook) as $key => $value) { - $modules[$key] = $module; - } - } - return $modules; -} diff --git a/includes/flag.services.inc b/includes/flag.services.inc deleted file mode 100644 index 872b560..0000000 --- a/includes/flag.services.inc +++ /dev/null @@ -1,90 +0,0 @@ -access($content_id, $action, $account)) { - // User has no permission to use this flag. - return FALSE; - } - return TRUE; - } -} - -/** - * Service wrapper to flag a content. - * - * @param $flag_name - * The flag name. - * @param $content_id - * The content ID to check if it was flagged. - * @param $uid - * The user ID to check if they flagged the content. - * @param $action - * The action. Should be "flag" or "unflag". - * @return - * TRUE if content was flagged. - */ -function flag_service_flag($flag_name, $content_id, $uid = NULL, $action = 'flag', $skip_permission_check = FALSE) { - global $user; - $account = empty($uid) ? $user : user_load($uid); - $flag = flag_get_flag($flag_name); - $skip_permission_check = (boolean) $skip_permission_check; - return $flag->flag($action, $content_id, $account, $skip_permission_check); -} - -/** - * Service wrapper to check if a content is flagged by a user. - * - * @param $flag_name - * The flag name. - * @param $content_id - * The content ID to check if it was flagged. - * @param $uid - * The user ID to check if they flagged the content. - * @return - * TRUE if content was flagged. - */ -function flag_service_is_flagged($flag_name, $content_id, $uid = NULL) { - $flag = flag_get_flag($flag_name); - return $flag->is_flagged($content_id, $uid); -} diff --git a/includes/flag.views.inc b/includes/flag.views.inc deleted file mode 100644 index 630ea26..0000000 --- a/includes/flag.views.inc +++ /dev/null @@ -1,255 +0,0 @@ - array( - 'path' => drupal_get_path('module', 'flag') . '/includes', - ), - 'handlers' => array( - 'flag_handler_relationship_content' => array( - 'parent' => 'views_handler_relationship', - 'file' => 'flag_handler_relationships.inc', - ), - 'flag_handler_relationship_counts' => array( - 'parent' => 'views_handler_relationship', - 'file' => 'flag_handler_relationships.inc', - ), - 'flag_handler_relationship_user_content' => array( - 'parent' => 'views_handler_relationship', - 'file' => 'flag_handler_relationships.inc', - ), - 'flag_handler_field_ops' => array( - 'parent' => 'views_handler_field', - ), - 'flag_handler_filter_flagged' => array( - 'parent' => 'views_handler_filter_boolean_operator', - ), - 'flag_handler_argument_content_id' => array( - 'parent' => 'views_handler_argument_numeric', - ), - ), - ); -} - -/** - * Implementation of hook_views_plugins(). - */ -function flag_views_plugins() { - return array( - 'argument validator' => array( - 'flag_flaggable_node' => array( - 'title' => t('Flaggable node'), - 'flag type' => 'node', - 'handler' => 'flag_plugin_argument_validate_flaggability', - 'path' => drupal_get_path('module', 'flag') . '/includes', - ), - 'flag_flaggable_user' => array( - 'title' => t('Flaggable user'), - 'flag type' => 'user', - 'handler' => 'flag_plugin_argument_validate_flaggability', - 'path' => drupal_get_path('module', 'flag') . '/includes', - ), - // A comment validator won't be very useful. Moreover, having it will - // contribute to the argument object's $options polution, so let's skip - // it. - ), - ); -} -/** - * Implementation of hook_views_data(). - */ -function flag_views_data() { - $data = array(); - - $data['flag_content']['table']['group'] = t('Flags'); - $data['flag_counts']['table']['group'] = t('Flags'); - - $data['flag_content']['uid'] = array( - 'title' => t('User'), - 'help' => t('The user that flagged an item.'), - 'relationship' => array( - 'base' => 'users', - 'handler' => 'views_handler_relationship', - 'label' => t('Flag user'), - ), - ); - - $data['flag_content']['timestamp'] = array( - 'title' => t('Flagged time'), - 'help' => t('Display the time the content was flagged by a user.'), - 'field' => array( - 'handler' => 'views_handler_field_date', - 'click sortable' => TRUE, - ), - 'sort' => array( - 'handler' => 'views_handler_sort_date', - ), - 'filter' => array( - 'handler' => 'views_handler_filter_date', - ), - 'argument' => array( - 'handler' => 'views_handler_argument_date', - ), - ); - - // Argument for content ID, used for "Who's flagged this" views. - $data['flag_content']['content_id'] = array( - 'title' => t('Content ID'), - 'help' => t('The unique ID of the object that has been flagged.'), - 'argument' => array( - 'handler' => 'flag_handler_argument_content_id', - ), - ); - - // Specialized is null/is not null filter. - $data['flag_content']['flagged'] = array( - 'title' => t('Flagged'), - 'help' => t('Filter to ensure content has or has not been flagged.'), - 'real field' => 'uid', - 'filter' => array( - 'handler' => 'flag_handler_filter_flagged', - 'label' => t('Flagged'), - ), - ); - - // Flag content links. - $data['flag_content']['ops'] = array( - 'title' => t('Flag link'), - 'help' => t('Display flag/unflag link.'), - 'field' => array( - 'handler' => 'flag_handler_field_ops', - ), - ); - - $data['flag_counts']['count'] = array( - 'title' => t('Flag counter'), - 'help' => t('The number of times a piece of content is flagged by any user.'), - 'field' => array( - 'handler' => 'views_handler_field_numeric', - 'click sortable' => TRUE, - ), - 'sort' => array( - 'handler' => 'views_handler_sort', - ), - 'filter' => array( - 'handler' => 'views_handler_filter_numeric', - ), - 'argument' => array( - 'handler' => 'views_handler_argument_numeric', - ), - ); - - return $data; -} - -/** - * Implementation of hook_views_data_alter(). - */ -function flag_views_data_alter(&$data) { - - foreach (array_keys(flag_fetch_definition()) as $flag_type) { - $flag = flag_flag::factory_by_content_type($flag_type); - $info = $flag->get_views_info(); - - // Add the flag relationship. - $data[$info['views table']]['flag_content_rel'] = array( - 'group' => t('Flags'), - 'title' => $info['title'], - 'help' => $info['help'], - 'relationship' => array( - 'flag type' => $flag_type, - 'handler' => 'flag_handler_relationship_content', - 'label' => t('flag'), - 'base' => 'flag_content', - 'base field' => 'content_id', - 'relationship field' => $info['join field'], - ), - ); - - // Add the flag counter relationship. - $data[$info['views table']]['flag_count_rel'] = array( - 'group' => t('Flags'), - 'title' => $info['counter title'], - 'help' => $info['counter help'], - 'relationship' => array( - 'flag type' => $flag_type, - 'handler' => 'flag_handler_relationship_counts', - 'label' => t('counter'), - 'base' => 'flag_counts', - 'base field' => 'content_id', - 'relationship field' => $info['join field'], - ), - ); - } - - // Add a relationship for the user that flagged any type of content. - $data['users']['flag_user_content_rel'] = array( - 'group' => t('Flags'), - 'title' => t("User's flagged content"), - 'help' => t('Limit results to users that have flagged certain content.'), - 'relationship' => array( - 'base' => 'flag_content', - 'base field' => 'uid', - 'relationship field' => 'uid', - 'handler' => 'flag_handler_relationship_user_content', - 'label' => t('user flagged content'), - ), - ); -} - -/** - * Implements hook_views_query_substitutions(). - * - * Allow replacement of current user's session id so we can cache these queries. - */ -function flag_views_query_substitutions() { - return array( - '***FLAG_CURRENT_USER_SID***' => flag_get_sid(), - ); -} -/** - * A helper function that creates a radio list of available flags. - * - * This function is used to select the desired flag when setting up flag - * relationships and fields. - */ -function flag_views_flag_config_form($form_type, $content_type, $current_flag) { - $flags = flag_get_flags($content_type); - - $options = array(); - foreach ($flags as $flag) { - $options[$flag->name] = $flag->get_title(); - } - - $form = array( - '#type' => $form_type, - '#title' => t('Flag'), - '#options' => $options, - '#default_value' => $current_flag, - '#required' => TRUE, - ); - - return $form; -} - -/** - * Helper function that gets the first defined flag and returns its name. - */ -function flag_views_flag_default($content_type) { - static $default_flag = array(); - - if (!array_key_exists($content_type, $default_flag)) { - $flag = array_shift(flag_get_flags($content_type)); - $default_flag[$content_type] = $flag ? $flag->name : NULL; - } - - return $default_flag[$content_type]; -} diff --git a/includes/flag.views_convert.inc b/includes/flag.views_convert.inc deleted file mode 100644 index 4cc815d..0000000 --- a/includes/flag.views_convert.inc +++ /dev/null @@ -1,234 +0,0 @@ -add_item() won't produce - * the right results, usually needed to set field options or values. - */ -function flag_views_convert($display, $type, &$view, $field, $id) { - static $flag_name; - - // First, replace any sign of views_bookmark with flag's Views 1 equivelant. - $key_search = array( - '^views_bookmark_ops_([0-9]+)' => 'flag_ops_', - '^views_bookmark_nodes_([0-9]+)' => 'flag_content_', - '^views_bookmark_users_([0-9]+)' => 'flag_users_', - '^views_bookmark_node_count_([0-9]+)' => 'flag_counts_', - ); - foreach ($field as $property => $value) { - foreach ($key_search as $search => $replace) { - if (!empty($value) && is_string($value) && preg_match('/'. $search .'/', $value, $matches)) { - $flag = flag_get_flag(NULL, $matches[1]); - $replace = $replace . $flag->name; - $field[$property] = preg_replace('/'. $search .'/', $replace, $value); - } - } - } - - // Create a table/field identifier for this field. - foreach (array('flag_ops_', 'flag_content_', 'flag_users_', 'flag_counts_') as $table) { - if (strpos($field['tablename'], $table) === 0) { - $name = str_replace($table, '', $field['tablename']); - if (!empty($name) && !isset($flag_name)) { - $flag_name = $name; - } - } - } - - // Now update values, options, etc. to those selected in the imported view. - switch ($type) { - case 'field': - switch ($id) { - case 'ops': - $new_field = array( - 'label' => $field['label'], - 'id' => 'ops', - 'table' => 'flag_content', - 'field' => 'ops', - 'relationship' => 'flag_content_rel', - ); - $new_rel = 'flag_content_rel'; - $flag_content_rel_user = 'current'; - break; - case 'count': - $new_field = array( - 'label' => $field['label'], - 'id' => 'count', - 'table' => 'flag_counts', - 'field' => 'count', - 'relationship' => 'flag_counts_rel', - ); - $new_rel = 'flag_counts_rel'; - break; - case 'name': - $new_field = array( - 'label' => $field['label'], - 'link_to_user' => 1, - 'id' => 'name', - 'table' => 'users', - 'field' => 'name', - 'relationship' => 'uid', - ); - $new_rel = 'uid'; - break; - } - break; - - case 'filter': - case 'exposed_filter': - switch ($id) { - case 'uid': - // The flag_content uid filter means "Include content only flagged by - // the current user". In D6, this is no longer a filter, but instead - // part of the relationship. Make the relationship join on the uid. - if ($field['value'] == '***CURRENT_USER***') { - $new_rel = 'flag_content_rel'; - $flag_content_rel_user = 'current'; - } - // Remove the old filter. - $view->set_item('default', $type, $id, NULL); - break; - case 'timestamp': - $new_field = array( - 'operator' => $field['operator'], - 'value' => flag_views_convert_timestamp_value($field['value']), - 'group' => 0, - 'id' => 'timestamp', - 'table' => 'flag_content', - 'field' => 'timestamp', - 'relationship' => 'flag_content_rel', - 'exposed' => $type == 'exposed_filter' ? 1 : 0, - ); - $new_rel = 'flag_content_rel'; - drupal_set_message(t('Flag is not able to convert the Flagged time filter. It\'s value is currently empty, but needs to be populated to work properly.'), 'warning'); - break; - case 'count': - $new_field = array( - 'operator' => $field['operator'], - 'value' => array('value' => $field['value']), - 'group' => 0, - 'id' => 'count', - 'table' => 'flag_counts', - 'field' => 'count', - 'relationship' => 'flag_counts_rel', - 'exposed' => $type == 'exposed_filter' ? 1 : 0, - ); - $new_rel = 'flag_counts_rel'; - break; - } - break; - - case 'argument': - // Flag in Drupal 5 only provides one argument, and in Views 1 arguments - // weren't given and ID, so we use the field type. - if (strpos($field['type'], 'flag_content_') === 0) { - $new_field = array( - 'id' => 'uid', - 'table' => 'users', - 'field' => 'uid', - 'relationship' => 'uid', - ); - $new_rel = 'uid'; - } - break; - - case 'sort': - switch ($id) { - case 'count': - $new_field = array( - 'order' => $field['sortorder'], - 'id' => 'count', - 'table' => 'flag_counts', - 'field' => 'count', - 'relationship' => 'flag_counts_rel', - ); - $new_rel = 'flag_counts_rel'; - break; - case 'timestamp': - $new_field = array( - 'order' => $field['sortorder'], - 'id' => 'timestamp', - 'table' => 'flag_content', - 'field' => 'timestamp', - 'relationship' => 'flag_content_rel', - ); - $new_rel = 'flag_content_rel'; - break; - } - break; - } - - // Add any new fields. - if (isset($new_field)) { - // Exposed filters are now wrapped into filters. - $type = $type == 'exposed_filter' ? 'filter' : $type; - // Update the type in the view options. - $view->set_item('default', $type, $id, $new_field); - } - - // Add any new relationships (but only once per view). - if (isset($new_rel) && !isset($view->display[$display]->display_options['relationships'][$new_rel])) { - if ($new_rel == 'flag_content_rel') { - $view->display[$display]->display_options['relationships'][$new_rel] = array( - 'label' => $flag_name, - 'required' => 1, - 'flag' => $flag_name, - 'user_scope' => 'any', - 'id' => 'flag_content_rel', - 'table' => 'node', - 'field' => 'flag_content_rel', - 'relationship' => 'none', - ); - } - elseif ($new_rel == 'flag_counts_rel') { - $view->display[$display]->display_options['relationships'][$new_rel] = array( - 'label' => $flag_name .' counts', - 'required' => 0, - 'flag' => $flag_name, - 'id' => 'flag_counts_rel', - 'table' => 'node', - 'field' => 'flag_counts_rel', - 'relationship' => 'none', - ); - } - elseif ($new_rel == 'uid') { - $view->display[$display]->display_options['relationships'][$new_rel] = array( - 'label' => isset($flag_name) ? $flag_name .' user' : 'user', - 'required' => 0, - 'id' => 'uid', - 'table' => 'flag_content', - 'field' => 'uid', - 'relationship' => 'flag_content_rel', - ); - } - } - - // Modify relationships if needed. - if (isset($flag_content_rel_user) && isset($view->display[$display]->display_options['relationships']['flag_content_rel'])) { - $view->display[$display]->display_options['relationships']['flag_content_rel']['user_scope'] = 'current'; - } -} - -/** - * In Views 1, dates were simply stored as a single string. In Views 2, the - * value is stored as an array of options. - */ -function flag_views_convert_timestamp_value($value) { - $type = 'date'; - if (stripos($value, 'now') !== FALSE) { - $type = 'offset'; - $value = trim(str_ireplace('now', '', $value)); - } - return array( - 'type' => $type, - 'value' => $value, - 'min' => '', - 'max' => '', - ); -} diff --git a/includes/flag.views_default.inc b/includes/flag.views_default.inc deleted file mode 100644 index 21acacc..0000000 --- a/includes/flag.views_default.inc +++ /dev/null @@ -1,279 +0,0 @@ - array( - 'id' => 'type', - 'table' => 'node', - 'field' => 'type', - 'label' => 'Type', - ), - 'title' => array( - 'id' => 'title', - 'table' => 'node', - 'field' => 'title', - 'label' => 'Title', - 'link_to_node' => 1, - ), - 'name' => array( - 'label' => 'Author', - 'link_to_user' => 1, - 'id' => 'name', - 'table' => 'users', - 'field' => 'name', - ), - ); - - $style_options = array( - 'grouping' => '', - 'override' => 0, - 'sticky' => 1, - 'columns' => array(), - 'default' => 'title', - 'order' => 'asc', - 'columns' => array( - 'type' => 'type', - 'title' => 'title', - 'name' => 'name', - ), - 'info' => array( - 'type' => array( - 'sortable' => TRUE, - ), - 'title' => array( - 'sortable' => TRUE, - ), - 'name' => array( - 'sortable' => TRUE, - ), - ), - 'override' => FALSE, - 'order' => 'asc', - ); - - $filters = array( - 'status' => array( - 'operator' => '=', - 'value' => 1, - 'group' => '0', - 'exposed' => FALSE, - 'expose' => array( - 'operator' => FALSE, - 'label' => '', - ), - 'id' => 'status', - 'table' => 'node', - 'field' => 'status', - 'relationship' => 'none', - ), - ); - - $relationships = array( - 'flag_content_rel' => array( - 'label' => 'bookmarks', - 'required' => 1, - 'flag' => 'bookmarks', - 'user_scope' => 'current', - 'id' => 'flag_content_rel', - 'table' => 'node', - 'field' => 'flag_content_rel', - 'relationship' => 'none', - 'override' => array( - 'button' => 'Override', - ), - ), - ); - - $access = array( - 'type' => 'role', - 'role' => drupal_map_assoc($flag->roles['flag']), - 'perm' => '', - ); - - // Additional fields and style options if comment exists. - if (module_exists('comment')) { - $fields += array( - 'comment_count' => array( - 'id' => 'comment_count', - 'table' => 'node_comment_statistics', - 'field' => 'comment_count', - 'label' => 'Replies', - ), - 'last_comment_timestamp' => array( - 'id' => 'last_comment_timestamp', - 'table' => 'node_comment_statistics', - 'field' => 'last_comment_timestamp', - 'label' => 'Last Post', - ), - ); - - $style_options['default'] = 'last_comment_timestamp'; - $style_options['order'] = 'desc'; - $style_options['info'] += array( - 'comment_count' => array( - 'sortable' => TRUE, - ), - 'last_comment_timestamp' => array( - 'sortable' => TRUE, - ), - ); - $style_options['columns'] += array( - 'comment_count' => 'comment_count', - 'last_comment_timestamp' => 'last_comment_timestamp', - ); - } - - $views = array(); - - /* Individual users user/%/bookmarks tab. */ - - // Additional relationship for this view. - $relationships_tab = $relationships; - $relationships_tab['flag_content_rel']['user_scope'] = 'any'; - $relationships_tab += array( - 'uid' => array( - 'label' => 'bookmarks_user', - 'required' => 1, - 'id' => 'uid', - 'table' => 'flag_content', - 'field' => 'uid', - 'relationship' => 'flag_content_rel', - ), - ); - - // Additional argument for this view. - $arguments_tab = array( - 'uid' => array( - 'default_action' => 'empty', - 'style_plugin' => 'default_summary', - 'style_options' => array(), - 'wildcard' => 'all', - 'wildcard_substitution' => 'All', - 'title' => '%1\'s bookmarks', - 'default_argument_type' => 'fixed', - 'default_argument' => '', - 'validate_type' => 'none', - 'validate_fail' => 'not found', - 'break_phrase' => 0, - 'not' => 0, - 'id' => 'uid', - 'table' => 'users', - 'field' => 'uid', - 'override' => array( - 'button' => 'Override', - ), - 'relationship' => 'uid', - 'default_options_div_prefix' => '', - 'default_argument_user' => 0, - 'default_argument_fixed' => '', - 'default_argument_php' => '', - ), - ); - - $view = new view; - $view->name = 'flag_bookmarks_tab'; - $view->description = 'Provides a tab on all user\'s profile pages containing bookmarks for that user.'; - $view->tag = 'flag'; - $view->view_php = ''; - $view->base_table = 'node'; - $view->is_cacheable = FALSE; - $view->api_version = 2; - $view->disabled = FALSE; - $handler = $view->new_display('default', 'Defaults', 'default'); - $handler->override_option('relationships', $relationships_tab); - $handler->override_option('fields', $fields); - $handler->override_option('arguments', $arguments_tab); - $handler->override_option('filters', $filters); - $handler->override_option('access', $access); - $handler->override_option('title', 'User bookmarks'); - $handler->override_option('empty', 'This user has not yet bookmarked any content.'); - $handler->override_option('empty_format', filter_fallback_format()); - $handler->override_option('items_per_page', '25'); - $handler->override_option('use_pager', TRUE); - $handler->override_option('style_plugin', 'table'); - $handler->override_option('style_options', $style_options); - - $handler = $view->new_display('page', 'Page', 'page'); - $handler->override_option('path', 'user/%/bookmarks'); - $handler->override_option('menu', array( - 'type' => 'tab', - 'title' => 'Bookmarks', - 'weight' => '0', - 'name' => 'navigation', - )); - $handler->override_option('tab_options', array( - 'type' => 'none', - 'title' => NULL, - 'weight' => NULL, - )); - - $views[$view->name] = $view; - - /* User bookmarks page with Ops. */ - - // Add some unique options for this view. - $style_options['columns'] += array('ops' => 'ops'); - $fields += array( - 'ops' => array( - 'label' => 'Ops', - 'id' => 'ops', - 'table' => 'flag_content', - 'field' => 'ops', - 'relationship' => 'flag_content_rel', - ), - ); - - $view = new view; - $view->name = 'flag_'. $flag->name; - $view->description = 'A page listing the current user\'s bookmarks at /bookmarks.'; - $view->tag = 'flag'; - $view->view_php = ''; - $view->base_table = 'node'; - $view->is_cacheable = '0'; - $view->api_version = 2; - $view->disabled = FALSE; - $handler = $view->new_display('default', 'Defaults', 'default'); - $handler->override_option('relationships', $relationships); - $handler->override_option('fields', $fields); - $handler->override_option('filters', $filters); - $handler->override_option('access', $access); - $handler->override_option('title', t('My bookmarks')); - $handler->override_option('items_per_page', '25'); - $handler->override_option('use_pager', TRUE); - $handler->override_option('empty', 'You have not yet bookmarked any content. Click the "'. $flag->flag_short .'" link when viewing a piece of content to add it to this list.'); - $handler->override_option('empty_format', filter_fallback_format()); - $handler->override_option('style_plugin', 'table'); - $handler->override_option('style_options', $style_options); - $handler = $view->new_display('page', 'Page', 'page'); - $handler->override_option('path', 'bookmarks'); - $handler->override_option('menu', array( - 'type' => 'normal', - 'title' => t('My bookmarks'), - 'weight' => '0', - )); - $handler->override_option('tab_options', array( - 'type' => 'none', - 'title' => NULL, - 'weight' => NULL, - )); - - $views[$view->name] = $view; - - return $views; -} diff --git a/includes/flag_handler_argument_content_id.inc b/includes/flag_handler_argument_content_id.inc deleted file mode 100644 index ee18743..0000000 --- a/includes/flag_handler_argument_content_id.inc +++ /dev/null @@ -1,46 +0,0 @@ -view->relationship[$this->options['relationship']]->get_flag(); - } - - /** - * Override the behavior of title(). Get the title of the appropriate objects. - */ - function title_query() { - if (!($flag = $this->get_flag())) { - return array(); // Error message is printed by get_flag(). - } - $views_info = $flag->get_views_info(); - - $titles = array(); - $placeholders = implode(', ', array_fill(0, sizeof($this->value), '%d')); - - $result = db_select($views_info['views table'], 'o') - ->fields('o', array($views_info['title field'])) - ->condition('o.' . $views_info['join field'], $this->value, 'IN') - ->execute(); - foreach ($result as $title) { - $titles[] = check_plain($title->$views_info['title field']); - } - return $titles; - } -} diff --git a/includes/flag_handler_field_ops.inc b/includes/flag_handler_field_ops.inc deleted file mode 100644 index 116ff67..0000000 --- a/includes/flag_handler_field_ops.inc +++ /dev/null @@ -1,146 +0,0 @@ -view->relationship[$this->options['relationship']])) { - return $this->view->relationship[$this->options['relationship']]->get_flag(); - } - } - - /** - * Return the the relationship we're linked to. That is, the alias for its - * table (which is suitbale for use with the various methods of the 'query' - * object). - */ - function get_parent_relationship() { - $parent = $this->view->relationship[$this->options['relationship']]->options['relationship']; - if (!$parent || $parent == 'none') { - return NULL; // Base query table. - } - else { - return $this->view->relationship[$parent]->alias; - } - } - - function option_definition() { - $options = parent::option_definition(); - $options['link_type'] = array('default' => ''); - return $options; - } - - function options_form(&$form, &$form_state) { - parent::options_form($form, $form_state); - - $form['link_type'] = array( - '#type' => 'radios', - '#title' => t('Link type'), - '#options' => array('' => t('Use flag link settings')) + _flag_link_type_options(), - '#default_value' => $this->options['link_type'], - ); - } - - /** - * Override base ::query(). The purpose here is to make it possible for the - * render() method to know two things: what's the content ID, and whether - * it's flagged. - */ - function query() { - if (!($flag = $this->get_flag())) { - return; // Error message is printed in render(). - } - $info = $flag->get_views_info(); - $parent = $this->get_parent_relationship(); - - // Find out if the content is flagged. We can't just peek at some field in - // our loaded table because it doesn't always reflect the user browsing the - // page. So we explicitly add the flag_content table to find this out. - $join = new views_join(); - $join->construct('flag_content', $info['views table'], $info['join field'], 'content_id'); - $join->extra[] = array( - 'field' => 'fid', - 'value' => $flag->fid, - 'numeric' => TRUE, - ); - if (!$flag->global) { - $join->extra[] = array( - 'field' => 'uid', - 'value' => '***CURRENT_USER***', - 'numeric' => TRUE, - ); - $join->extra[] = array( - 'field' => 'sid', - 'value' => '***FLAG_CURRENT_USER_SID***', - 'numeric' => TRUE, - ); - } - $flag_table = $this->query->add_table('flag_content', $parent, $join); - $this->aliases['is_flagged'] = $this->query->add_field($flag_table, 'content_id'); - - // Next, find out the content ID. We can't add_field() on this table - // (flag_content), because its content_id may be NULL (in case no user has - // flagged this content, and it's a LEFT JOIN). So we reach to the parent - // relationship and add_field() *its* content ID column. - $left_table = $this->view->relationship[$this->options['relationship']]->table_alias; - $this->aliases['content_id'] = $this->query->add_field($left_table, $info['join field']); - } - - /** - * Find out if the flag applies to each item seen on the page. It's done in a - * separate DB query to to avoid complexity and to make 'many to one' tests - * (e.g. checking user roles) possible without causing duplicate rows. - */ - function pre_render($values) { - if (!($flag = $this->get_flag())) { - return; // Error message is printed in render(). - } - - $ids = array(); - foreach ($values as $row) { - $content_id = $row->{$this->aliases['content_id']}; - $is_flagged = $row->{$this->aliases['is_flagged']}; - if (isset($content_id)) { - $ids[$content_id] = $is_flagged ? 'unflag' : 'flag'; - } - } - $this->flag_applies = $ids ? $flag->access_multiple($ids) : array(); - } - - function render($values) { - if (!($flag = $this->get_flag())) { - return t('Missing flag'); // get_flag() itself will print a more detailed message. - } - - $content_id = $values->{$this->aliases['content_id']}; - $is_flagged = $values->{$this->aliases['is_flagged']}; - - if (empty($this->flag_applies[$content_id])) { - // Flag does not apply to this content. - return; - } - - if (!empty($this->options['link_type'])) { - $flag->link_type = $this->options['link_type']; - } - return $flag->theme($is_flagged ? 'unflag' : 'flag', $content_id); - } -} diff --git a/includes/flag_handler_filter_flagged.inc b/includes/flag_handler_filter_flagged.inc deleted file mode 100644 index ea04d93..0000000 --- a/includes/flag_handler_filter_flagged.inc +++ /dev/null @@ -1,36 +0,0 @@ - 1); - return $options; - } - - function options_form(&$form, &$form_state) { - parent::options_form($form, $form_state); - $form['value']['#type'] = 'radios'; - $form['value']['#title'] = t('Status'); - $form['value']['#options'] = array(1 => t('Flagged'), 0 => t('Not flagged')); - if ($this->options['expose']['optional']) { - $form['value']['#options']['All'] = t('All'); - } - $form['value']['#default_value'] = empty($this->options['value']) ? 0 : $this->options['value']; - $form['value']['#description'] = '

' . t('This filter is only needed if the relationship used has the "Include only flagged content" option unchecked. Otherwise, this filter is useless, because all records are already limited to flagged content.') . '

' . t('By choosing Not flagged, it is possible to create a list of content that is specifically not flagged.', array('@unflagged-url' => 'http://drupal.org/node/299335')) . '

'; - } - - function query() { - $operator = $this->value ? 'IS NOT' : 'IS'; - $this->query->add_where($this->options['group'], $this->relationship . '.uid', NULL, $operator . ' NULL'); - } -} diff --git a/includes/flag_handler_relationships.inc b/includes/flag_handler_relationships.inc deleted file mode 100644 index 0ac60af..0000000 --- a/includes/flag_handler_relationships.inc +++ /dev/null @@ -1,266 +0,0 @@ - NULL); - $options['required'] = array('default' => 1); - return $options; - } - - /** - * Make sure the flag exists. - * - * When importing views, or when deleting flags, inconsistent views may - * result. This validator is called by Views before saving or previewing a - * view. - */ - function validate() { - $errors = array(); - $tokens = array( - '@relationship-name' => $this->ui_name() . ' ' . $this->admin_summary(), - '@flag-name' => $this->options['flag'], - ); - if (!$this->options['flag']) { - $errors[] = t('You must pick a flag to use for the relationship "@relationship-name".', $tokens); - } - else if (!flag_get_flag($this->options['flag'])) { - $errors[] = t('This view is looking for a flag by the name "@flag-name", but there is no such flag. Perhaps it was deleted. Please update the relationship "@relationship-name" in this view to use an existing flag.', $tokens); - } - return $errors; - } - - function get_flag_type() { - return isset($this->definition['flag type']) ? $this->definition['flag type'] : NULL; - } - - /** - * Returns the flag object. - */ - function get_flag() { - - // Backward compatibility: There may exist old views on the system whose - // 'flag' option isn't set. (This happens if the admin had skippped - // clicking the 'Update' button.) When run, these views should behave as - // if the first flag was selected. - if (!isset($this->options['flag'])) { - $this->options['flag'] = flag_views_flag_default($this->get_flag_type()); - } - - // Validation: Since validate() is called only when in Views's - // administrative UI, we need to do validation at "run time" ourselves. - if (($errors = $this->validate())) { - foreach ($errors as $error) { - drupal_set_message($error, 'error'); - } - } - - return flag_get_flag($this->options['flag']); - } - - // @todo: It's logical that this class should also implement options_form(), - // to show the flag selector, and query(), to filter on the flag. -} - -/** - * Specialized relationship handler associating flags and content. - * - * @ingroup views - */ -class flag_handler_relationship_content extends flag_handler_relationship { - - function option_definition() { - $options = parent::option_definition(); - $options['user_scope'] = array('default' => 'current'); - return $options; - } - - function options_form(&$form, &$form_state) { - parent::options_form($form, $form_state); - $content_type = $this->definition['flag type']; - $form['label']['#description'] .= ' '. t('The name of the selected flag makes a good label.'); - $form['flag'] = flag_views_flag_config_form('radios', $content_type, $this->options['flag']); - - $form['user_scope'] = array( - '#type' => 'radios', - '#title' => t('By'), - '#options' => array('current' => t('Current user'), 'any' => t('Any user')), - '#default_value' => $this->options['user_scope'], - ); - - $form['required']['#title'] = t('Include only flagged content'); - $form['required']['#description'] = t('If checked, only content that has this flag will be included. Leave unchecked to include all content; or, in combination with the Flagged filter, to limit the results to specifically unflagged content.', array('@unflagged-url' => 'http://drupal.org/node/299335')); - - if (!$form['flag']['#options']) { - $form = array( - 'error' => array( - '#markup' => '

' . t('No %type flags exist. You must first create a %type flag before being able to use this relationship type.', array('%type' => $content_type, '@create-url' => url(FLAG_ADMIN_PATH))) . '

', - ), - ); - $form_state['no flags exist'] = TRUE; - } - if (module_exists('session_api')) { - $form['session_warning'] = array( - '#markup' => '

' . t('Warning: Adding this relationship for any flag that contains anonymous flagging access will disable page caching for anonymous users when this view is executed. (But this is only true when the relationship is constrained to "Current user", not to "Any user".) It is recommended to create a dedicated page for views containing anonymous user data.') . '

', - ); - } - } - - function options_validate($form, &$form_state) { - if (!empty($form_state['no flags exist'])) { - form_error($form, t('You must first create a flag')); - } - } - - function admin_summary() { - return $this->options['user_scope'] == 'current' ? t('by current user') : t('by any user'); - } - - function ui_name() { - // We put the bookmark name in the UI string to save space. - return t('!group: !title', array('!group' => $this->definition['group'], '!title' => empty($this->options['flag']) ? t('(Please select a flag)') : $this->options['flag'])); - } - - /** - * Called to implement a relationship in a query. - */ - function query() { - if (!($flag = $this->get_flag())) { - return; - } - - $this->definition['extra'][] = array( - 'field' => 'fid', - 'value' => $flag->fid, - 'numeric' => TRUE, - ); - if ($this->options['user_scope'] == 'current' && !$flag->global) { - $this->definition['extra'][] = array( - 'field' => 'uid', - 'value' => '***CURRENT_USER***', - 'numeric' => TRUE, - ); - if (array_search(DRUPAL_ANONYMOUS_RID, $flag->roles['flag']) !== FALSE) { - // Disable page caching for anonymous users. - $GLOBALS['conf']['cache'] = 0; - - // Add in the SID from Session API for anonymous users. - $this->definition['extra'][] = array( - 'field' => 'sid', - 'value' => '***FLAG_CURRENT_USER_SID***', - 'numeric' => TRUE, - ); - } - } - parent::query(); - } -} - -/** - * Specialized relationship handler associating flag counts and content. - * - * @ingroup views - */ -class flag_handler_relationship_counts extends flag_handler_relationship { - - function options_form(&$form, &$form_state) { - parent::options_form($form, $form_state); - $content_type = $this->definition['flag type']; - $form['flag'] = flag_views_flag_config_form('radios', $content_type, $this->options['flag']); - - $form['required']['#title'] = t('Include only flagged content'); - $form['required']['#description'] = t('If checked, only content that is flagged will be included.'); - } - - function admin_summary() { - // Nothing to show. - } - - function ui_name() { - // We put the bookmark name in the UI string to save space. - return t('!group: !title counter', array('!group' => $this->definition['group'], '!title' => $this->options['flag'])); - } - - /** - * Called to implement a relationship in a query. - */ - function query() { - if (!($flag = $this->get_flag())) { - return; - } - - $this->definition['extra'][] = array( - 'field' => 'fid', - 'value' => $flag->fid, - 'numeric' => TRUE, - ); - if (!empty($this->options['required'])) { - // Unfortunately, we may have zeros in our table, so having - // parent::query() do INNER JOIN doesn't suffice. We need to filter these - // zeros out. - // @todo Make sure zero records aren't written in the first place, and - // remove this code. - $this->definition['extra'][] = array( - 'field' => 'count', - 'operator' => '>', - 'value' => '0', - 'numeric' => TRUE, - ); - } - parent::query(); - } -} - -/** - * Specialized relationship handler associating flags and users. - * - * @ingroup views - */ -class flag_handler_relationship_user_content extends flag_handler_relationship { - - function options_form(&$form, &$form_state) { - parent::options_form($form, $form_state); - $form['label']['#description'] .= ' '. t('Including name of the selected flag helps identify this relationship.'); - - $form['flag'] = flag_views_flag_config_form('radios', NULL, $this->options['flag']); - $form['flag']['#title'] = t('Flagged'); - - $form['required']['#title'] = t('Include only users who have flagged content.'); - $form['required']['#description'] = t('If checked, only users that have flagged any content with this flag will be included.'); - } - - function admin_summary() { - return $this->options['flag']; - } - - /** - * Called to implement a relationship in a query. - */ - function query() { - if (!($flag = $this->get_flag())) { - return; - } - - $this->definition['extra'][] = array( - 'field' => 'fid', - 'value' => $flag->fid, - 'numeric' => TRUE, - ); - parent::query(); - } -} - diff --git a/includes/flag_plugin_argument_validate_flaggability.inc b/includes/flag_plugin_argument_validate_flaggability.inc deleted file mode 100644 index 1efe9af..0000000 --- a/includes/flag_plugin_argument_validate_flaggability.inc +++ /dev/null @@ -1,233 +0,0 @@ -flag_type = $this->definition['flag type']; - } - - function option_definition() { - $options = parent::option_definition(); - $options['flag_name'] = array('default' => '*relationship*'); - $options['flag_test'] = array('default' => 'flaggable'); - $options['flag_id_type'] = array('default' => 'id'); - return $options; - } - - function options_form(&$form, &$form_state) { - $options = $this->flags_options(); - - $form[$this->_option_name('flag_name')] = array( - '#type' => 'radios', - '#title' => t('Flag'), - '#options' => $options, - '#default_value' => $this->_get_option('flag_name', '*relationship*'), - '#description' => t('Select the flag to validate against.'), - ); - if (!$options) { - $form[$this->_option_name('flag_name')]['#description'] = '

' . t('No %type flags exist. You must first create a %type flag before being able to use one.', array('%type' => $this->flag_type, '@create-url' => FLAG_ADMIN_PATH)) . '

'; - } - - $form[$this->_option_name('flag_test')] = array( - '#type' => 'select', - '#title' => t('Validate the @type only if', array('@type' => $this->flag_type)), - '#options' => $this->tests_options(), - '#default_value' => $this->_get_option('flag_test', 'flaggable'), - ); - - // This validator supports the "multiple IDs" syntax. It may not be - // extremely useful, but similar validators do support this and it's a good - // thing to be compatible. - $form[$this->_option_name('flag_id_type')] = array( - '#type' => 'select', - '#title' => t('Argument type'), - '#options' => array( - 'id' => t('ID'), - 'ids' => t('IDs separated by , or +'), - ), - '#default_value' => $this->_get_option('flag_id_type', 'id'), - ); - } - - /** - * Returns form #options for the flags. Returns empty array if no flags were - * found. - */ - function flags_options() { - $flags = flag_get_flags($this->flag_type); - if (!$flags) { - return array(); - } - else { - foreach ($flags as $flag) { - $options[$flag->name] = $flag->get_title(); - } - $options['*relationship*'] = t('Pick the first flag mentioned in the relationships.'); - return $options; - } - } - - // This function seems useless, but in the Drupal 6 branch it allows for - // compatibility between Views 3 and Views 2. - function _get_option($option, $default) { - return $this->options[$option]; - } - - // This function seems useless, but in the Drupal 6 branch it allows for - // compatibility between Views 3 and Views 2. - function _option_name($option) { - return $option; - } - - /** - * Migrate existing Views 2 options to Views 3. - */ - function convert_options(&$options) { - $prefix = 'validate_argument_' . $this->flag_type . '_'; - if (!isset($options['flag_name']) && !empty($this->argument->options[$prefix . 'flag_name'])) { - $options['flag_name'] = $this->argument->options[$prefix . 'flag_name']; - $options['flag_test'] = $this->argument->options[$prefix . 'flag_test']; - $options['flag_id_type'] = $this->argument->options[$prefix . 'flag_id_type']; - } - } - - /** - * Declares all tests. This scheme makes it easy for derived classes to add - * and remove tests. - */ - function tests_info($which = NULL) { - return array( - 'flaggable' => array( - 'title' => t('It is flaggable'), - 'callback' => 'test_flaggable', - ), - 'flagged' => array( - 'title' => t('It is flagged at least once'), - 'callback' => 'test_flagged', - ), - 'flagged_by_current_user' => array( - 'title' => t('It is flagged by the current user'), - 'callback' => 'test_flagged_by_current_user', - ), - ); - } - - function tests_options() { - $options = array(); - foreach ($this->tests_info() as $id => $info) { - $options[$id] = $info['title']; - } - return $options; - } - - function get_flag() { - $flag_name = $this->_get_option('flag_name', '*relationship*'); - - if ($flag_name == '*relationship*') { - // Pick the first flag mentioned in the relationships. - foreach ($this->view->relationship as $id => $handler) { - // Note: we can't do $handler->field, because the relationship handler's init() may overwrite it. - if (strpos($handler->options['field'], 'flag') !== FALSE && !empty($handler->options['flag'])) { - $flag = flag_get_flag($handler->options['flag']); - if ($flag && $flag->content_type == $this->flag_type) { - return $flag; - } - } - } - } - - return flag_get_flag($flag_name); - } - - /** - * Tests whether the argument is flaggable, or flagged, or flagged by current - * user. These are three possible tests, and which of the three to actually - * carry out is determined by 'flag_test'. - */ - function validate_argument($argument) { - $flag_test = $this->_get_option('flag_test', 'flaggable'); - $id_type = $this->_get_option('flag_id_type', 'id'); - - $flag = $this->get_flag(); - if (!$flag) { - // Validator is misconfigured somehow. - return TRUE; - } - - if ($id_type == 'id') { - if (!is_numeric($argument)) { - // If a user is being smart and types several IDs where only one is - // expected, we invalidate this. - return FALSE; - } - } - - $ids = views_break_phrase($argument, $this); - if ($ids->value == array(-1)) { - // Malformed argument syntax. Invalidate. - return FALSE; - } - $ids = $ids->value; - - // Delegate the testing to the particual test method. $passed then - // holds the IDs that passed the test. - $tests_info = $this->tests_info(); - $method = $tests_info[$flag_test]['callback']; - if (method_exists($this, $method)) { - $passed = $this->$method($ids, $flag); - } - else { - $passed = array(); - } - - // If some IDs exist that haven't $passed our test then validation fails. - $failed = array_diff($ids, $passed); - return empty($failed); - } - - // - // The actual tests. They return the IDs that passed. - // - - function test_flaggable($ids, $flag) { - return array_filter($ids, array($flag, 'applies_to_content_id')); - } - - function test_flagged($ids, $flag) { - // view_break_phrase() is guaranteed to return only integers, so this is SQL safe. - $flattened_ids = implode(',', $ids); - return $this->_test_by_sql("SELECT content_id FROM {flag_counts} WHERE fid = :fid AND content_id IN ($flattened_ids) AND count > 0", array(':fid' => $flag->fid)); - } - - function test_flagged_by_current_user($ids, $flag) { - global $user; - if (!$user->uid) { - // Anonymous user - return FALSE; - } - $flattened_ids = implode(',', $ids); - return $this->_test_by_sql("SELECT content_id FROM {flag_content} WHERE fid = :fid AND content_id IN ($flattened_ids) AND uid = :uid", array(':fid' => $flag->fid, ':uid' => $user->uid)); - } - - // Helper: executes an SQL query and returns all the content_id's. - function _test_by_sql($sql, $args) { - $passed = array(); - $result = db_query($sql, $args); - foreach ($result as $row) { - $passed[] = $row->content_id; - } - return $passed; - } -} - diff --git a/src/ActionLinkPluginManager.php b/src/ActionLinkPluginManager.php new file mode 100644 index 0000000..f577b36 --- /dev/null +++ b/src/ActionLinkPluginManager.php @@ -0,0 +1,48 @@ +alterInfo('flag_link_type_info'); + $this->setCacheBackend($cache_backend, 'flag_link_type_plugins'); + } + + /** + * Get an array of all link type labels keyed by plugin ID. + * + * @return array + * An array of all link type plugins. + */ + public function getAllLinkTypes() { + $link_types = []; + foreach ($this->getDefinitions() as $plugin_id => $plugin_def) { + $link_types[$plugin_id] = t($plugin_def['label']); + } + + return $link_types; + } + +} diff --git a/src/ActionLinkTypeBase.php b/src/ActionLinkTypeBase.php new file mode 100644 index 0000000..85856ba --- /dev/null +++ b/src/ActionLinkTypeBase.php @@ -0,0 +1,193 @@ +configuration += $this->defaultConfiguration(); + } + + /** + * Returns a route name given an $action. + * + * @param string|null $action + * A string containing the action name. + * + * @return string + * A string containing a route name. + */ + abstract public function routeName($action = NULL); + + /** + * {@inheritdoc} + */ + public function buildLink($action, FlagInterface $flag, EntityInterface $entity) { + $parameters = [ + 'flag_id' => $flag->id(), + 'entity_id' => $entity->id(), + ]; + + return new Url($this->routeName($action), $parameters); + } + + /** + * Helper method to generate a destination URL parameter. + * + * @return string + * A string containing a destination URL parameter. + */ + protected function getDestination() { + $current_url = Url::fromRoute(''); + $route_params = $current_url->getRouteParameters(); + + if (isset($route_params['destination'])) { + return $route_params['destination']; + } + + $current_path = ltrim($current_url->toString(), '/'); + return $current_path; + } + + /** + * {@inheritdoc} + */ + public function renderLink($action, FlagInterface $flag, EntityInterface $entity) { + $url = $this->buildLink($action, $flag, $entity); + + $url->setRouteParameter('destination', $this->getDestination()); + + $render = $url->toRenderArray(); + $render['#type'] = 'link'; + $render['#attributes']['id'] = 'flag-' . $flag->id() . '-id-' . $entity->id(); + + if ($action === 'unflag') { + $render['#title'] = $flag->getUnflagShortText(); + $render['#alt'] = $flag->getUnflagLongText(); + } + else { + $render['#title'] = $flag->getFlagShortText(); + $render['#alt'] = $flag->getFlagLongText(); + } + + return $render; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * Provides a form array for the action link plugin's settings form. + * + * Derived classes will want to override this method. + * + * @param array $form + * The form array. + * @param FormStateInterface $form_state + * The form state. + * + * @return array + * The modified form array. + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + return $form; + } + + /** + * Processes the action link setting form submit. + * + * Derived classes will want to override this method. + * + * @param array $form + * The form array. + * @param FormStateInterface $form_state + * The form state. + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + // Override this. + } + + /** + * Validates the action link setting form. + * + * Derived classes will want to override this method. + * + * @param array $form + * The form array. + * @param FormStateInterface $form_state + * The form state. + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + // Override this. + } + + /** + * Provides the action link plugin's default configuration. + * + * Derived classes will want to override this method. + * + * @return array + * The plugin configuration array. + */ + public function defaultConfiguration() { + return []; + } + + /** + * Provides the action link plugin's current configuration array. + * + * @return array + * An array containing the plugin's current configuration. + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * Updates the plugin's current configuration. + * + * @param array $configuration + * An array containing the plugin's configuration. + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + +} diff --git a/src/ActionLinkTypePluginInterface.php b/src/ActionLinkTypePluginInterface.php new file mode 100644 index 0000000..0e79efc --- /dev/null +++ b/src/ActionLinkTypePluginInterface.php @@ -0,0 +1,50 @@ +flag($flag_id, $entity_id); + + $flag = $flagging->getFlag(); + $entity = $flagging->getFlaggable(); + + return $this->generateResponse('unflag', $flag, $entity); + } + + /** + * Performs an unflagging when called via a route. + * + * This method is invoked when a user clicks an AJAX unflagging link provided + * by the AJAXactionLink plugin. + * + * @param int $flag_id + * The flag ID. + * @param int $entity_id + * The entity ID to unflag. + * + * @return AjaxResponse + * The response object. + * + * @see \Drupal\flag\Plugin\ActionLink\AJAXactionLink + */ + public function unflag($flag_id, $entity_id) { + $flag_service = \Drupal::service('flag'); + $flag_service->unflag($flag_id, $entity_id); + + $flag = $flag_service->getFlagById($flag_id); + $entity = $flag_service->getFlaggableById($flag, $entity_id); + + return $this->generateResponse('flag', $flag, $entity); + } + + /** + * Generates a response object after handing the un/flag request. + * + * @param string $action + * The action being performed, should be 'flag' or 'unflag'. + * @param FlagInterface $flag + * The flag entity. + * @param EntityInterface $entity + * The entity object. + * + * @return AjaxResponse + * The response object. + */ + protected function generateResponse($action, FlagInterface $flag, EntityInterface $entity) { + // Create a new AJAX response. + $response = new AjaxResponse(); + + // Get the link type plugin. + $link_type = $flag->getLinkTypePlugin(); + + // Generate the link render array and get the link CSS ID. + $link = $link_type->renderLink($action, $flag, $entity); + $link_id = '#' . $link['#attributes']['id']; + + // Create a new JQuery Replace command to update the link display. + $replace = new ReplaceCommand($link_id, drupal_render($link)); + $response->addCommand($replace); + + return $response; + } + +} diff --git a/src/Controller/FieldEntryFormController.php b/src/Controller/FieldEntryFormController.php new file mode 100644 index 0000000..e3f4577 --- /dev/null +++ b/src/Controller/FieldEntryFormController.php @@ -0,0 +1,151 @@ +currentUser(); + $flag = Flag::load($flag_id); + + $flagging = $this->entityManager()->getStorage('flagging')->create([ + 'fid' => $flag->id(), + 'entity_type' => $flag->getFlaggableEntityType(), + 'entity_id' => $entity_id, + 'type' => $flag->id(), + 'uid' => $account->id(), + ]); + + return $this->getForm($flagging, 'add'); + } + + /** + * Return the flagging edit form. + * + * @param string $flag_id + * The flag ID. + * @param mixed $entity_id + * The entity ID. + * + * @return array + * The flagging edit form. + */ + public function edit($flag_id, $entity_id) { + $flagging = $this->getFlagging($flag_id, $entity_id); + + return $this->getForm($flagging, 'edit'); + } + + /** + * Performs an unflagging when called via a route. + * + * @param int $flag_id + * The flag ID. + * @param int $entity_id + * The entity ID to unflag. + * + * @return AjaxResponse + * The response object. + * + * @see \Drupal\flag\Plugin\ActionLink\AJAXactionLink + */ + public function unflag($flag_id, $entity_id) { + $flagging = $this->getFlagging($flag_id, $entity_id); + + return $this->getForm($flagging, 'delete'); + } + + /** + * Title callback when creating a new flagging. + * + * @param int $flag_id + * The flag ID. + * @param int $entity_id + * The entity ID to unflag. + * + * @return string + * The flag field entry form title. + */ + public function flagTitle($flag_id, $entity_id) { + $flag = \Drupal::service('flag')->getFlagById($flag_id); + $link_type = $flag->getLinkTypePlugin(); + return $link_type->getFlagQuestion(); + } + + /** + * Title callback when editing an existing flagging. + * + * @param int $flag_id + * The flag ID. + * @param int $entity_id + * The entity ID to unflag. + * + * @return string + * The flag field entry form title. + */ + public function editTitle($flag_id, $entity_id) { + $flag = \Drupal::service('flag')->getFlagById($flag_id); + $link_type = $flag->getLinkTypePlugin(); + return $link_type->getEditFlaggingTitle(); + } + + /** + * Get a flagging that already exists. + * + * @param string $flag_id + * The flag ID. + * @param mixed $entity_id + * The flaggable ID. + * + * @return FlaggingInterface|null + * The flagging or NULL. + */ + protected function getFlagging($flag_id, $entity_id) { + $account = $this->currentUser(); + $flag = \Drupal::service('flag')->getFlagById($flag_id); + $entity = \Drupal::service('flag')->getFlaggableById($flag, $entity_id); + $flaggings = \Drupal::service('flag')->getFlaggings($entity, $flag, $account); + + return reset($flaggings); + } + + /** + * Get the flag's field entry form. + * + * @param FlaggingInterface $flagging + * The flagging from which to get the form. + * @param string|null $operation + * The operation identifying the form variant to return. + * + * return array + * The form array. + */ + protected function getForm(FlaggingInterface $flagging, $operation = 'default') { + return $this->entityFormBuilder()->getForm($flagging, $operation); + } +} diff --git a/src/Controller/FlagListController.php b/src/Controller/FlagListController.php new file mode 100644 index 0000000..d162f0c --- /dev/null +++ b/src/Controller/FlagListController.php @@ -0,0 +1,125 @@ +getPermissions() as $perm => $pinfo) { + $roles = user_roles(FALSE, $perm); + + foreach ($roles as $rid => $role) { + $all_roles[$rid] = $role->label(); + } + } + + $out = implode(', ', $all_roles); + + if (empty($out)) { + return String::placeHolder($this->t('None')); + } + + return rtrim($out, ', '); + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::buildRow(). + */ + public function buildRow(EntityInterface $entity) { + + $row['label'] = $this->getLabel($entity); + + $row['roles'] = $this->getFlagRoles($entity); + + $row['is_global'] = $entity->isGlobal() ? t('Yes') : t('No'); + + $row['status'] = $entity->isEnabled() ? t('enabled') : t('disabled'); + + return $row + parent::buildRow($entity); + } + + /** + * Overrides Drupal\Core\Entity\EntityListController::render(). + * + * We override the render() method to add helpful text below the entity list. + */ + public function render() { + $build['table'] = parent::render(); + + $output = ""; + + // @todo Move this too hook_help()? + if (!\Drupal::moduleHandler()->moduleExists('views')) { + $output .= '

' . t('The Views module is not installed, or not enabled. It is recommended that you install the Views module to be able to easily produce lists of flagged content.', ['@views-url' => Url::fromUri('http://drupal.org/project/views')]) . '

'; + } + else { + $output .= '

'; + $output .= t('Lists of flagged content can be displayed using views. You can configure these in the Views administration section.', ['@views-url' => Url::fromRoute('views_ui.list')->toString()]); + if (\Drupal::service('flag')->getFlagById('bookmarks')) { + $output .= ' ' . t('Flag module automatically provides a few default views for the bookmarks flag. You can use these as templates by cloning these views and then customizing as desired.', ['@views-url' => Url::fromRoute('views_ui.list', ['query' => ['tag' => 'flag']])->toString()]); + } + $output .= ' ' . t('The Flag module handbook contains extensive documentation on creating customized views using flags.', ['@flag-handbook-url' => 'http://drupal.org/handbook/modules/flag', '@customize-url' => 'http://drupal.org/node/296954']); + $output .= '

'; + } + + if (!\Drupal::moduleHandler()->moduleExists('rules')) { + $output .= '

' . t('Flagging an item may trigger rules. However, you don\'t have the Rules module enabled, so you won\'t be able to enjoy this feature. The Rules module is a more extensive solution than Flag actions.', ['@rules-url' => Url::fromUri('http://drupal.org/node/407070')->toString()]) . '

'; + } + + /* + * Commented out because the rules route name has yet to be established. + else { + $output .= '

' . t('Flagging an item may trigger rules.', ['@rules-url' => Url::fromUri('base://admin/config/workflow/rules')]) . '

'; + } + */ + + $output .= '

' . t('To learn about the various ways to use flags, please check out the Flag module handbook.', ['@handbook-url' => 'http://drupal.org/handbook/modules/flag']) . '

'; + + $build['markup'] = [ + '#type' => 'markup', + '#markup' => $output, + ]; + + return $build; + } + +} diff --git a/src/Controller/ReloadLinkController.php b/src/Controller/ReloadLinkController.php new file mode 100644 index 0000000..730928e --- /dev/null +++ b/src/Controller/ReloadLinkController.php @@ -0,0 +1,102 @@ +flagService = $flag; + } + + /** + * Create. + * + * @param ContainerInterface $container + * The container object. + * + * @return ReloadLinkController + * The reload link controller. + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('flag') + ); + } + + /** + * Performs a flagging when called via a route. + * + * @param int $flag_id + * The flag ID. + * @param int $entity_id + * The flaggable ID. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * The response object. + * + * @see \Drupal\flag\Plugin\Reload + */ + public function flag($flag_id, $entity_id) { + /* @var \Drupal\flag\FlaggingInterface $flagging */ + $flagging = $this->flagService->flag($flag_id, $entity_id); + + // Redirect back to the entity. A passed in destination query parameter + // will automatically override this. + $url_info = $flagging->getFlaggable()->urlInfo(); + return $this->redirect($url_info->getRouteName(), $url_info->getRouteParameters()); + } + + /** + * Performs a flagging when called via a route. + * + * @param int $flag_id + * The flag ID. + * @param int $entity_id + * The flagging ID. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * The response object. + * + * @see \Drupal\flag\Plugin\Reload + */ + public function unflag($flag_id, $entity_id) { + $this->flagService->unflag($flag_id, $entity_id); + + $flag = $this->flagService->getFlagById($flag_id); + $entity = $this->flagService->getFlaggableById($flag, $entity_id); + + // Redirect back to the entity. A passed in destination query parameter + // will automatically override this. + $url_info = $entity->urlInfo(); + return $this->redirect($url_info->getRouteName(), $url_info->getRouteParameters()); + } + +} diff --git a/src/Entity/Flag.php b/src/Entity/Flag.php new file mode 100644 index 0000000..91169b9 --- /dev/null +++ b/src/Entity/Flag.php @@ -0,0 +1,571 @@ +flag_type) { + $this->flagTypeCollection = new DefaultSingleLazyPluginCollection( + \Drupal::service('plugin.manager.flag.flagtype'), + $this->flag_type, $this->flagTypeConfig + ); + } + + if ($this->link_type) { + $this->linkTypeCollection = new DefaultSingleLazyPluginCollection( + \Drupal::service('plugin.manager.flag.linktype'), + $this->link_type, $this->linkTypeConfig + ); + } + } + + /** + * {@inheritdoc} + */ + public function enable() { + $this->enabled = TRUE; + } + + /** + * {@inheritdoc} + */ + public function disable() { + $this->enabled = FALSE; + } + + /** + * {@inheritdoc} + */ + public function isEnabled() { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function isFlagged(EntityInterface $entity, AccountInterface $account = NULL) { + if ($account == NULL) { + $account = \Drupal::currentUser(); + } + + $result = \Drupal::entityQuery('flagging') + ->condition('uid', $account->id()) + ->condition('fid', $this->id()) + ->condition('entity_type', $entity->getEntityTypeId()) + ->condition('entity_id', $entity->id()) + ->execute(); + + if (!empty($result)) { + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getFlaggableEntityType() { + return $this->entity_type; + } + + /** + * {@inheritdoc} + */ + public function getPluginCollections() { + return [ + 'flagTypeConfig' => $this->flagTypeCollection, + 'linkTypeConfig' => $this->linkTypeCollection, + ]; + } + + /** + * {@inheritdoc} + */ + public function getFlagTypePlugin() { + return $this->flagTypeCollection->get($this->flag_type); + } + + /** + * {@inheritdoc} + */ + public function setFlagTypePlugin($plugin_id) { + $this->flag_type = $plugin_id; + // $this->flagTypeBag->addInstanceId($pluginID); + // Workaround for https://www.drupal.org/node/2288805 + $this->flagTypeCollection = new DefaultSingleLazyPluginCollection( + \Drupal::service('plugin.manager.flag.flagtype'), + $this->flag_type, $this->flagTypeConfig + ); + + // Get the entity type from the plugin definition. + $plugin = $this->getFlagTypePlugin(); + $plugin_def = $plugin->getPluginDefinition(); + $this->entity_type = $plugin_def['entity_type']; + } + + /** + * {@inheritdoc} + */ + public function getLinkTypePlugin() { + return $this->linkTypeCollection->get($this->link_type); + } + + /** + * {@inheritdoc} + */ + public function setlinkTypePlugin($plugin_id) { + $this->link_type = $plugin_id; + + // $this->linkTypeBag->addInstanceId($pluginID); + // Workaround for https://www.drupal.org/node/2288805 + $this->linkTypeCollection = new DefaultSingleLazyPluginCollection( + \Drupal::service('plugin.manager.flag.linktype'), + $this->link_type, $this->linkTypeConfig + ); + } + + /** + * {@inheritdoc} + */ + public function getPermissions() { + return [ + "flag $this->id" => [ + 'title' => t('Flag %flag_title', [ + '%flag_title' => $this->label, + ]), + ], + "unflag $this->id" => [ + 'title' => t('Unflag %flag_title', [ + '%flag_title' => $this->label, + ]), + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function hasActionAccess($action, AccountInterface $account = NULL) { + if ($action === 'flag' || $action === 'unflag') { + $account = $account ?: \Drupal::currentUser(); + return $account->hasPermission($action . ' ' . $this->id); + } + else { + // @todo: Is this the correct response? + return FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function isGlobal() { + return $this->is_global; + } + + /** + * {@inheritdoc} + */ + public function setGlobal($is_global = TRUE) { + if ($is_global) { + $this->is_global = TRUE; + } + else { + $this->is_global = FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function setFlagShortText($text) { + $this->flag_short; + } + + /** + * {@inheritdoc} + */ + public function getFlagShortText() { + return $this->flag_short; + } + + /** + * {@inheritdoc} + */ + public function getFlagLongText() { + return $this->flag_long; + } + + /** + * {@inheritdoc} + */ + public function setFlagLongText($flag_long) { + $this->flag_long = $flag_long; + } + + /** + * {@inheritdoc} + */ + public function getFlagMessage() { + return $this->flag_message; + } + + /** + * {@inheritdoc} + */ + public function setFlagMessage($flag_message) { + $this->flag_message = $flag_message; + } + + /** + * {@inheritdoc} + */ + public function getUnflagLongText() { + return $this->unflag_long; + } + + /** + * {@inheritdoc} + */ + public function setUnflagLongText($unflag_long) { + $this->unflag_long = $unflag_long; + } + + /** + * {@inheritdoc} + */ + public function getUnflagMessage() { + return $this->unflag_message; + } + + /** + * {@inheritdoc} + */ + public function setUnflagMessage($unflag_message) { + $this->unflag_message = $unflag_message; + } + + /** + * {@inheritdoc} + */ + public function getUnflagShortText() { + return $this->unflag_short; + } + + /** + * {@inheritdoc} + */ + public function setUnflagShortText($unflag_short) { + $this->unflag_short = $unflag_short; + } + + /** + * {@inheritdoc} + */ + public function getUnflagDeniedText() { + return $this->unflag_denied_text; + } + + /** + * {@inheritdoc} + */ + public function setUnflagDeniedText($unflag_denied_text) { + $this->unflag_denied_text = $unflag_denied_text; + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->weight; + } + + /** + * {@inheritdoc} + */ + public function setWeight($weight) { + $this->weight = $weight; + } + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + /* + // Save the Flag Type configuration. + $flagTypePlugin = $this->getFlagTypePlugin(); + $this->set('flagTypeConfig', $flagTypePlugin->getConfiguration()); + + // Save the Link Type configuration. + $linkTypePlugin = $this->getLinkTypePlugin(); + $this->set('linkTypeConfig', $linkTypePlugin->getConfiguration()); + */ + // Reset the render cache for the entity. + // @todo Inject the entity manager into the object? + \Drupal::entityManager() + ->getViewBuilder($this->getFlaggableEntityType()) + ->resetCache(); + // Clear entity extra field caches. + \Drupal::entityManager()->clearCachedFieldDefinitions(); + + } + + /** + * {@inheritdoc} + */ + public static function preDelete(EntityStorageInterface $storage, array $entities) { + parent::preDelete($storage, $entities); + + foreach ($entities as $entity) { + \Drupal::service('event_dispatcher') + ->dispatch(FlagEvents::FLAG_DELETED, new FlagDeleteEvent($entity)); + } + } + + /** + * Sorts the flag entities, putting disabled flags at the bottom. + * + * @see \Drupal\Core\Config\Entity\ConfigEntityBase::sort() + */ + public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) { + + // Check if the entities are flags, if not go with the default. + if ($a instanceof FlagInterface && $b instanceof FlagInterface) { + + if ($a->isEnabled() && $b->isEnabled()) { + return parent::sort($a, $b); + } + elseif (!$a->isEnabled()) { + return -1; + } + elseif (!$b->isEnabled()) { + return 1; + } + } + + return parent::sort($a, $b); + } + + /** + * {@inheritdoc} + */ + public function toArray() { + // @todo Do we need Flag::toArray() any longer? + $properties = parent::toArray(); + $names = [ + 'flag_type', + 'link_type', + 'flagTypeConfig', + 'linkTypeConfig', + ]; + + foreach ($names as $name) { + $properties[$name] = $this->get($name); + } + + return $properties; + } + +} diff --git a/src/Entity/FlagDisabledException.php b/src/Entity/FlagDisabledException.php new file mode 100644 index 0000000..90030a9 --- /dev/null +++ b/src/Entity/FlagDisabledException.php @@ -0,0 +1,31 @@ + $flag->label()))); + } + +} \ No newline at end of file diff --git a/src/Entity/Flagging.php b/src/Entity/Flagging.php new file mode 100644 index 0000000..a07c680 --- /dev/null +++ b/src/Entity/Flagging.php @@ -0,0 +1,145 @@ +get('fid')->value; + } + + /** + * Gets the parent flag entity. + * + * @return \Drupal\Core\Entity\EntityInterface|\Drupal\flag\FlagInterface + * The flag related this this flagging. + */ + public function getFlag() { + return $this->entityManager()->getStorage('flag')->load($this->getFlagId()); + } + + /** + * Gets the entity type of the flaggable. + * + * @return string + * A string containing the flaggable type ID. + */ + public function getFlaggableType() { + return $this->get('entity_type')->value; + } + + /** + * Gets the entity ID of the flaggable. + * + * @return string + * A string containing the flaggable ID. + */ + public function getFlaggableId() { + return $this->get('entity_id')->value; + } + + /** + * Gets the flaggable entity. + * + * @return \Drupal\Core\Entity\EntityInterface + * The flaggable entity. + */ + public function getFlaggable() { + return $this->entityManager()->getStorage($this->getFlaggableType())->load($this->getFlaggableId()); + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields['id'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Flagging ID')) + ->setDescription(t('The flagging ID.')) + ->setReadOnly(TRUE); + + $fields['uuid'] = BaseFieldDefinition::create('uuid') + ->setLabel(t('UUID')) + ->setDescription(t('The flagging UUID.')) + ->setReadOnly(TRUE); + + $fields['fid'] = BaseFieldDefinition::create('string') + ->setLabel(t('Flag ID')) + ->setDescription(t('The Flag ID.')) + ->setReadOnly(TRUE); + + $fields['entity_type'] = BaseFieldDefinition::create('string') + ->setLabel(t('Entity Type')) + ->setDescription(t('The Entity Type.')); + + $fields['entity_id'] = BaseFieldDefinition::create('string') + ->setLabel(t('Entity ID')) + ->setDescription(t('The Entity ID.')); + + $fields['type'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Type')) + ->setDescription(t('The flag type.')) + ->setSetting('target_type', 'flag') + ->setReadOnly(TRUE); + + $fields['uid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('User ID')) + ->setDescription(t('The user ID of the flagging user.')) + ->setSettings([ + 'target_type' => 'user', + 'default_value' => 0, + ]); + + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Created')) + ->setDescription(t('The time that the flagging was created.')); + + return $fields; + } + +} diff --git a/src/Event/FlagDeleteEvent.php b/src/Event/FlagDeleteEvent.php new file mode 100644 index 0000000..9c27fbc --- /dev/null +++ b/src/Event/FlagDeleteEvent.php @@ -0,0 +1,26 @@ +flag = $flag; + } + +} diff --git a/src/Event/FlagEvents.php b/src/Event/FlagEvents.php new file mode 100644 index 0000000..10981e5 --- /dev/null +++ b/src/Event/FlagEvents.php @@ -0,0 +1,28 @@ +entity = $entity; + $this->action = $action; + } + +} diff --git a/src/FlagInterface.php b/src/FlagInterface.php new file mode 100644 index 0000000..2e0ad87 --- /dev/null +++ b/src/FlagInterface.php @@ -0,0 +1,269 @@ +getFlags(); + + // Provide flag and unflag permissions for each flag. + foreach ($flags as $flag_name => $flag) { + $permissions += $flag->getPermissions(); + } + + return $permissions; + } +} diff --git a/src/FlagService.php b/src/FlagService.php new file mode 100644 index 0000000..e3f3ef7 --- /dev/null +++ b/src/FlagService.php @@ -0,0 +1,459 @@ +flagTypeManager = $flag_type; + $this->eventDispatcher = $event_dispatcher; + $this->entityQueryManager = $entity_query; + $this->currentUser = $current_user; + $this->entityManager = $entity_manager; + } + + /** + * Get a flag type definition. + * + * @param string $entity_type + * (optional) The entity type to get the definition for, or NULL to return + * all flag types. + * + * @return array + * The flag type definition array. + * + * @see hook_flag_type_info() + */ + public function fetchDefinition($entity_type = NULL) { + // @todo Add caching, PLS! + if (!empty($entity_type)) { + return $this->flagTypeManager->getDefinition($entity_type); + } + + return $this->flagTypeManager->getDefinitions(); + } + + /** + * List all flags available. + * + * If node type or account are entered, a list of all possible flags will be + * returned. + * + * @param string $entity_type + * (optional) The type of entity for which to load the flags. + * @param string $bundle + * (optional) The bundle for which to load the flags. + * @param AccountInterface $account + * (optional) The user account to filter available flags. If not set, all + * flags for the given entity and bundle will be returned. + * + * @return array + * An array of the structure [fid] = flag_object. + */ + public function getFlags($entity_type = NULL, $bundle = NULL, AccountInterface $account = NULL) { + $query = $this->entityQueryManager->get('flag'); + + if ($entity_type != NULL) { + $query->condition('entity_type', $entity_type); + } + + if ($bundle != NULL && $entity_type != $bundle) { + $query->condition("types.$bundle", $bundle); + } + + $result = $query->execute(); + + $flags = $this->entityManager->getStorage('flag')->loadMultiple($result); + + if ($account == NULL) { + return $flags; + } + + $filtered_flags = []; + foreach ($flags as $flag) { + if ($flag->hasActionAccess('flag ' . $flag->id(), $account) || $flag->hasActionAccess('unflag ' . $flag->id(), $account)) { + $filtered_flags[] = $flag; + } + } + + return $filtered_flags; + } + + /** + * Get all flaggings for the given entity, flag, and optionally, user. + * + * @param EntityInterface $entity + * Optional. The flaggable entity. If NULL, flaggins for any entity will be + * returned. + * @param FlagInterface $flag + * Optional. The flag entity. If NULL, flaggings for any flag will be + * returned. + * @param AccountInterface $account + * Optional. The account of the flagging user. If NULL, flaggings for any + * user will be returned. + * + * @return array + * An array of flaggings. + */ + public function getFlaggings(EntityInterface $entity = NULL, FlagInterface $flag = NULL, AccountInterface $account = NULL) { + $query = $this->entityQueryManager->get('flagging'); + + if (!empty($account)) { + $query = $query->condition('uid', $account->id()); + } + + if (!empty($flag)) { + $query = $query->condition('fid', $flag->id()); + } + + if (!empty($entity)) { + $query = $query->condition('entity_type', $entity->getEntityTypeId()) + ->condition('entity_id', $entity->id()); + } + + $result = $query->execute(); + + $flaggings = []; + foreach ($result as $flagging_id) { + $flaggings[$flagging_id] = $this->entityManager->getStorage('flagging')->load($flagging_id); + } + + return $flaggings; + } + + /** + * Load the flag entity given the ID. + * + * @param int $flag_id + * The ID of the flag to load. + * + * @return FlagInterface|null + * The flag entity. + */ + public function getFlagById($flag_id) { + return $this->entityManager->getStorage('flag')->load($flag_id); + } + + /** + * Loads the flaggable entity given the flag entity and entity ID. + * + * @param FlagInterface $flag + * The flag entity. + * @param int $entity_id + * The ID of the flaggable entity. + * + * @return EntityInterface|null + * The flaggable entity object. + */ + public function getFlaggableById(FlagInterface $flag, $entity_id) { + return $this->entityManager->getStorage($flag->getFlaggableEntityType())->load($entity_id); + } + + /** + * Get a list of users that have flagged an entity. + * + * @param EntityInterface $entity + * The entity object. + * @param FlagInterface $flag + * Optional. The flag entity to which to restrict results. + * + * @return array + * An array of users who have flagged the entity. + */ + public function getFlaggingUsers(EntityInterface $entity, FlagInterface $flag = NULL) { + $query = $this->entityQueryManager->get('users') + ->condition('entity_type', $entity->getEntityTypeId()) + ->condition('entity_id', $entity->id()); + + if (!empty($flag)) { + $query = $query->condition('fid', $flag->id()); + } + + $result = $query->execute(); + + $flaggings = []; + foreach ($result as $flagging_id) { + $flaggings[$flagging_id] = $this->entityManager->getStorage('flagging')->load($flagging_id); + } + + return $flaggings; + } + + /** + * Flags the given entity given the flag and entity objects. + * + * @param FlagInterface $flag + * The flag entity. + * @param EntityInterface $entity + * The entity to flag. + * @param AccountInterface $account + * Optional. The account of the user flagging the entity. If not given, + * the current user is used. + * + * @throws \Drupal\flag\Entity\FlagDisabledException + * Thrown when a flag operation is being performed against a disabled flag. + * + * @return FlaggingInterface|null + * The flagging. + */ + public function flagByObject(FlagInterface $flag, EntityInterface $entity, AccountInterface $account = NULL) { + if (empty($account)) { + $account = $this->currentUser; + } + + // Throw an exception if the flag is disabled. + if (!$flag->isEnabled()) { + throw new FlagDisabledException($flag); + } + + $flagging = $this->entityManager->getStorage('flagging')->create([ + 'type' => 'flag', + 'uid' => $account->id(), + 'fid' => $flag->id(), + 'entity_id' => $entity->id(), + 'entity_type' => $entity->getEntityTypeId(), + ]); + + $flagging->save(); + + $this->incrementFlagCounts($flag, $entity); + + $this->entityManager + ->getViewBuilder($entity->getEntityTypeId()) + ->resetCache([ + $entity, + ]); + + $this->eventDispatcher->dispatch(FlagEvents::ENTITY_FLAGGED, new FlaggingEvent($flag, $entity, 'flag')); + + return $flagging; + } + + /** + * Flags an entity given the flag ID and entity ID. + * + * @param int $flag_id + * The ID of the flag. + * @param int $entity_id + * The ID of the entity to flag. + * @param AccountInterface $account + * Optional. The account of user flagging the entity. If not given, the + * current user is used. + * + * @return FlaggingInterface|null + * The flagging entity. + * + * @api + */ + public function flag($flag_id, $entity_id, AccountInterface $account = NULL) { + if (empty($account)) { + $account = $this->currentUser; + } + + $flag = $this->getFlagById($flag_id); + $entity = $this->getFlaggableById($flag, $entity_id); + + return $this->flagByObject($flag, $entity, $account); + } + + /** + * Unflags an entity given the flag ID and entity ID. + * + * @param int $flag_id + * The ID of the flag. + * @param int $entity_id + * The ID of the flagged entity to unflag. + * @param AccountInterface $account + * Optional. The account of the user that created the flagging. + * + * @return array + * An array of flagging IDs to delete. + * + * @api + */ + public function unflag($flag_id, $entity_id, AccountInterface $account = NULL) { + if (empty($account)) { + $account = $this->currentUser; + } + + $flag = $this->getFlagById($flag_id); + $entity = $this->getFlaggableById($flag, $entity_id); + + $this->decrementFlagCounts($flag, $entity); + + return $this->unflagByObject($flag, $entity, $account); + } + + /** + * Unflags the given entity for the given flag. + * + * @param FlagInterface $flag + * The flag being unflagged. + * @param EntityInterface $entity + * The entity to unflag. + * @param AccountInterface $account + * Optional. The account of the user that created the flagging. + * + * @throws \Drupal\flag\Entity\FlagDisabledException + * Thrown when an unflag is being performed on a disabled flag. + * + * @return array + * An array of flagging IDs to delete. + */ + public function unflagByObject(FlagInterface $flag, EntityInterface $entity, AccountInterface $account = NULL) { + // Throw an exception if the flag is disabled. + if (!$flag->isEnabled()) { + throw new FlagDisabledException($flag); + } + + $this->eventDispatcher->dispatch(FlagEvents::ENTITY_UNFLAGGED, new FlaggingEvent($flag, $entity, 'unflag')); + + $out = []; + $flaggings = $this->getFlaggings($entity, $flag, $account); + foreach ($flaggings as $flagging) { + $out[] = $flagging->id(); + + $this->unflagByFlagging($flagging); + } + + return $out; + } + + /** + * Deletes the given flagging. + * + * @param FlaggingInterface $flagging + * The flagging to delete. + */ + public function unflagByFlagging(FlaggingInterface $flagging) { + $flagging->delete(); + } + + /** + * Increments count of flagged entities. + * + * @param FlagInterface $flag + * The flag to increment. + * @param EntityInterface $entity + * The flaggable entity. + */ + protected function incrementFlagCounts(FlagInterface $flag, EntityInterface $entity) { + db_merge('flag_counts') + ->key([ + 'fid' => $flag->id(), + 'entity_id' => $entity->id(), + 'entity_type' => $entity->getEntityTypeId(), + ]) + ->fields([ + 'last_updated' => REQUEST_TIME, + 'count' => 1, + ]) + ->expression('count', 'count + :inc', [':inc' => 1]) + ->execute(); + } + + /** + * Reverts incrementation of count of flagged entities. + * + * @param FlagInterface $flag + * The flag to decrement. + * @param EntityInterface $entity + * The flaggable entity. + */ + protected function decrementFlagCounts(FlagInterface $flag, EntityInterface $entity) { + $count_result = db_select('flag_counts') + ->fields(NULL, ['fid', 'entity_id', 'entity_type', 'count']) + ->condition('fid', $flag->id()) + ->condition('entity_id', $entity->id()) + ->condition('entity_type', $entity->getEntityTypeId()) + ->execute() + ->fetchAll(); + if (count($count_result) == 1) { + db_delete('flag_counts') + ->condition('fid', $flag->id()) + ->condition('entity_id', $entity->id()) + ->condition('entity_type', $entity->getEntityTypeId()) + ->execute(); + } + else { + db_update('flag_counts') + ->expression('count', 'count - 1') + ->condition('fid', $flag->id()) + ->condition('entity_id', $entity->id()) + ->condition('entity_id', $entity->id()) + ->execute(); + } + } + +} diff --git a/src/FlagTypeBase.php b/src/FlagTypeBase.php new file mode 100644 index 0000000..24f1652 --- /dev/null +++ b/src/FlagTypeBase.php @@ -0,0 +1,118 @@ +configuration += $this->defaultConfiguration(); + } + + /** + * Provides the default configuration values for the flag type. + * + * @return array + * The flag type's default plugin configuration. + */ + public function defaultConfiguration() { + return []; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * Returns this flag type plugin's configuration array. + * + * @return array + * The plugin configuration array. + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * Replaces the plugin's configurations with those given in the parameter. + * + * @param array $configuration + * The plugin configuration array. + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * Provides a form for this action link plugin settings. + * + * The form provided by this method is displayed by the FlagAddForm when + * creating or editing the Flag. Derived classes should override this. + * + * @param array $form + * The form array. + * @param FormStateInterface $form_state + * The form state. + * + * @return array + * The form array + * @see \Drupal\flag\Form\FlagAddForm + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + return $form; + } + + /** + * Handles the form submit for this action link plugin. + * + * Derived classes will want to override this. + * + * @param array $form + * The form array. + * @param FormStateInterface $form_state + * The form state. + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + // Override this. + } + + /** + * Handles the validation for the action link plugin settings form. + * + * @param array $form + * The form array. + * @param FormStateInterface $form_state + * The form state. + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + // Override this. + } + + /** + * {@inheritdoc} + */ + public function typeAccessMultiple(array $entity_ids, AccountInterface $account) { + return []; + } +} diff --git a/src/FlagTypePluginInterface.php b/src/FlagTypePluginInterface.php new file mode 100644 index 0000000..0958348 --- /dev/null +++ b/src/FlagTypePluginInterface.php @@ -0,0 +1,37 @@ +alterInfo('flag_type_info'); + $this->setCacheBackend($cache_backend, 'flag'); + } + + /** + * Gets all flag types. + * + * @return array + * Returns all flag types. + */ + public function getAllFlagTypes() { + $flag_types = []; + + foreach ($this->getDefinitions() as $plugin_id => $plugin_def) { + $flag_types[$plugin_id] = $plugin_def['title']; + } + asort($flag_types); + + return $flag_types; + } + +} diff --git a/src/FlaggingAccessController.php b/src/FlaggingAccessController.php new file mode 100644 index 0000000..1338df0 --- /dev/null +++ b/src/FlaggingAccessController.php @@ -0,0 +1,47 @@ +hasActionAccess('flag')); + } + + /** + * Checks unflagging permission. + * + * @param Request $request + * The request object. + * + * @return string + * Returns indication value for unflagging access permission. + */ + public function checkUnflag($flag_id) { + $flag = Flag::load($flag_id); + return AccessResult::allowedIf($flag->hasActionAccess('unflag')); + } + +} diff --git a/src/FlaggingInterface.php b/src/FlaggingInterface.php new file mode 100644 index 0000000..9206b86 --- /dev/null +++ b/src/FlaggingInterface.php @@ -0,0 +1,33 @@ +get('flag'); + $step1_form = $tempstore->get('FlagAddPage'); + + $flag = $this->entity; + $flag->label = $step1_form['label']; + $flag->id = $step1_form['id']; + + $flag->setFlagTypePlugin($step1_form['flag_entity_type']); + $flag->setLinkTypePlugin($step1_form['flag_link_type']); + + // Mark the flag as new. + $flag->is_new = TRUE; + + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = t('Create Flag'); + return $actions; + } +} diff --git a/src/Form/FlagAddPageForm.php b/src/Form/FlagAddPageForm.php new file mode 100644 index 0000000..d158b2d --- /dev/null +++ b/src/Form/FlagAddPageForm.php @@ -0,0 +1,139 @@ + 'fieldset', + '#title' => t('Flag basic info'), + '#collapsable' => FALSE, + '#weight' => -10, + ]; + $form['flag_basic_info']['label'] = [ + '#type' => 'textfield', + '#title' => t('Label'), + '#description' => t('A short, descriptive title for this flag. It will be used in administrative interfaces to refer to this flag, and in page titles and menu items of some views this module provides (theses are customizable, though). Some examples could be Bookmarks, Favorites, or Offensive.', array('@insite-views-url' => Url::fromRoute('views_ui.list')->toString())), + '#maxlength' => 255, + '#required' => TRUE, + '#weight' => -3, + ]; + $form['flag_basic_info']['id'] = [ + '#type' => 'machine_name', + '#title' => t('Machine name'), + '#description' => t('The machine-name for this flag. It may be up to 32 characters long and may only contain lowercase letters, underscores, and numbers. It will be used in URLs and in all API calls.'), + '#weight' => -2, + '#machine_name' => [ + 'exists' => [$this, 'exists'], + 'source' => ['flag_basic_info', 'label'], + ], + ]; + + $form['flag_type_info'] = [ + '#type' => 'fieldset', + '#title' => t('Type and Action'), + '#attributes' => [ + 'class' => ['container-inline'], + ], + ]; + + $form['flag_type_info']['flag_entity_type'] = [ + '#type' => 'select', + '#title' => t('Flag'), + '#options' => \Drupal::service('plugin.manager.flag.flagtype')->getAllFlagTypes(), + '#default_value' => 'flagtype_node', + ]; + + $form['flag_type_info']['flag_link_type'] = [ + '#type' => 'select', + '#title' => t('using'), + '#options' => \Drupal::service('plugin.manager.flag.linktype')->getAllLinkTypes(), + '#default_value' => 'reload', + ]; + + $types = []; + + $plugins = \Drupal::service('plugin.manager.flag.flagtype')->getDefinitions(); + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => t('Continue'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + /* + $flag = AbstractFlag::factory_by_entity_type($form_state->getValue('type')); + if (get_class($flag) == 'BrokenFlag') { + form_set_error('type', + t("This flag type, %type, isn't valid.", + array('%type' => $form_state->getValue('type')))); + } + */ + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $form_state->setRedirect('flag.add', [ + 'entity_type' => $form_state->getValue('flag_entity_type') + ]); + + $tempstore = \Drupal::service('user.tempstore')->get('flag'); + $tempstore->set('FlagAddPage', $form_state->getValues()); + } + + /** + * Determines if the flag already exists. + * + * @param string $id + * The flag ID + * + * @return bool + * TRUE if the flag exists, FALSE otherwise. + */ + public function exists($id) { + // @todo: Make this injected like ActionFormBase::exists(). + return \Drupal::entityManager()->getStorage('flag')->load($id); + } +} diff --git a/src/Form/FlagDeleteForm.php b/src/Form/FlagDeleteForm.php new file mode 100644 index 0000000..1042fa3 --- /dev/null +++ b/src/Form/FlagDeleteForm.php @@ -0,0 +1,60 @@ + $this->entity->label(), + ]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('flag.list'); + } + + /** + * {@inheritdoc} + */ + public function submit(array $form, FormStateInterface $form_state) { + $this->entity->delete(); + drupal_set_message(t('Flag %label was deleted.', [ + '%label' => $this->entity->label(), + ])); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/src/Form/FlagDisableConfirmForm.php b/src/Form/FlagDisableConfirmForm.php new file mode 100644 index 0000000..e7ac0e8 --- /dev/null +++ b/src/Form/FlagDisableConfirmForm.php @@ -0,0 +1,79 @@ +flag = $flag; + return parent::buildForm($form, $form_state); + } + + public function getFormID() { + return 'flag_disable_confirm_form'; + } + + public function getQuestion() { + if ($this->flag->isEnabled()) { + return t('Disable flag @name?', array('@name' => $this->flag->label())); + } + + return t('Enable flag @name?', array('@name' => $this->flag->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('flag.list'); + } + + public function getDescription() { + if ($this->flag->isEnabled()) { + return t('Users will no longer be able to use the flag, but no data will be lost.'); + } + + return t('The flag will appear once more on configured nodes.'); + } + + public function getConfirmText() { + if ($this->flag->isEnabled()) { + return $this->t('Disable'); + } + + return $this->t('Enable'); + } + + public function submitForm(array &$form, FormStateInterface $form_state) { + // Toggle the flag state. + if ($this->flag->isEnabled()) { + $this->flag->disable(); + } + else { + $this->flag->enable(); + } + + // Invalidate the flaggable render cache. + \Drupal::entityManager() + ->getViewBuilder($this->flag->entity_type) + ->resetCache(); + + // Save The flag entity. + $this->flag->save(); + + // Redirect to the flag admin page. + $form_state->setRedirect('flag.list'); + } + +} diff --git a/src/Form/FlagEditForm.php b/src/Form/FlagEditForm.php new file mode 100644 index 0000000..7695520 --- /dev/null +++ b/src/Form/FlagEditForm.php @@ -0,0 +1,32 @@ +entity; + + $form['#flag'] = $flag; + $form['#flag_name'] = $flag->id; + + $form['label'] = [ + '#type' => 'textfield', + '#title' => t('Label'), + '#default_value' => $flag->label, + '#description' => t('A short, descriptive title for this flag. It will be used in administrative interfaces to refer to this flag, and in page titles and menu items of some views this module provides (theses are customizable, though). Some examples could be Bookmarks, Favorites, or Offensive.', array('@insite-views-url' => Url::fromRoute('views_ui.list')->toString())), + '#maxlength' => 255, + '#required' => TRUE, + '#weight' => -3, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#title' => t('Machine name'), + '#default_value' => $flag->id, + '#description' => t('The machine-name for this flag. It may be up to 32 characters long and may only contain lowercase letters, underscores, and numbers. It will be used in URLs and in all API calls.'), + '#weight' => -2, + '#machine_name' => [ + 'exists' => [$this, 'exists'], + ], + '#disabled' => !$flag->isNew(), + '#required' => TRUE, + ]; + + $form['is_global'] = [ + '#type' => 'checkbox', + '#title' => t('Global flag'), + '#default_value' => $flag->isGlobal(), + '#description' => t('If checked, flag is considered "global" and each entity is either flagged or not. If unchecked, each user has individual flags on entities.'), + '#weight' => -1, + ]; + + $form['messages'] = [ + '#type' => 'fieldset', + '#title' => t('Messages'), + ]; + + $flag_short = $flag->getFlagShortText(); + $form['messages']['flag_short'] = [ + '#type' => 'textfield', + '#title' => t('Flag link text'), + '#default_value' => !empty($flag_short) ? $flag_short : t('Flag this item'), + '#description' => t('The text for the "flag this" link for this flag.'), + '#required' => TRUE, + ]; + + $form['messages']['flag_long'] = [ + '#type' => 'textfield', + '#title' => t('Flag link description'), + '#default_value' => $flag->getFlagLongText(), + '#description' => t('The description of the "flag this" link. Usually displayed on mouseover.'), + ]; + + $form['messages']['flag_message'] = [ + '#type' => 'textfield', + '#title' => t('Flagged message'), + '#default_value' => $flag->getFlagMessage(), + '#description' => t('Message displayed after flagging content. If JavaScript is enabled, it will be displayed below the link. If not, it will be displayed in the message area.'), + ]; + + $unflag_short = $flag->getUnflagShortText(); + $form['messages']['unflag_short'] = [ + '#type' => 'textfield', + '#title' => t('Unflag link text'), + '#default_value' => !empty($unflag_short) ? $unflag_short : t('Unflag this item'), + '#description' => t('The text for the "unflag this" link for this flag.'), + '#required' => TRUE, + ]; + + $form['messages']['unflag_long'] = [ + '#type' => 'textfield', + '#title' => t('Unflag link description'), + '#default_value' => $flag->getUnflagLongText(), + '#description' => t('The description of the "unflag this" link. Usually displayed on mouseover.'), + ]; + + $form['messages']['unflag_message'] = [ + '#type' => 'textfield', + '#title' => t('Unflagged message'), + '#default_value' => $flag->getUnflagMessage(), + '#description' => t('Message displayed after content has been unflagged. If JavaScript is enabled, it will be displayed below the link. If not, it will be displayed in the message area.'), + ]; + + $form['access'] = [ + '#type' => 'fieldset', + '#title' => t('Flag access'), + '#tree' => FALSE, + '#weight' => 10, + ]; + + // Switch plugin type in case a different is chosen. + + $flag_type_plugin = $flag->getFlagTypePlugin(); + $flag_type_def = $flag_type_plugin->getPluginDefinition(); + + $bundles = entity_get_bundles($flag_type_def['entity_type']); + $entity_bundles = []; + foreach ($bundles as $bundle_id => $bundle_row) { + $entity_bundles[$bundle_id] = $bundle_row['label']; + } + + // Flag classes will want to override this form element. + $form['access']['types'] = [ + '#type' => 'checkboxes', + '#title' => t('Flaggable types'), + '#options' => $entity_bundles, + '#default_value' => $flag->types, + '#description' => t('Check any sub-types that this flag may be used on.'), + '#required' => TRUE, + '#weight' => 10, + ]; + + $form['access']['unflag_denied_text'] = [ + '#type' => 'textfield', + '#title' => t('Unflag not allowed text'), + '#default_value' => $flag->getUnflagDeniedText(), + '#description' => t('If a user is allowed to flag but not unflag, this text will be displayed after flagging. Often this is the past-tense of the link text, such as "flagged".'), + '#weight' => -1, + ]; + + $form['display'] = [ + '#type' => 'fieldset', + '#title' => t('Display options'), + '#description' => t('Flags are usually controlled through links that allow users to toggle their behavior. You can choose how users interact with flags by changing options here. It is legitimate to have none of the following checkboxes ticked, if, for some reason, you wish to place the the links on the page yourself.', array('@placement-url' => 'http://drupal.org/node/295383')), + '#tree' => FALSE, + '#weight' => 20, + // @todo: Move flag_link_type_options_states() into controller? + // '#after_build' => array('flag_link_type_options_states'), + ]; + + $form['display']['settings'] = [ + '#type' => 'container', + '#prefix' => '', + '#weight' => 21, + ]; + + $form = $flag_type_plugin->buildConfigurationForm($form, $form_state); + + $form['display']['link_type'] = [ + '#type' => 'radios', + '#title' => t('Link type'), + '#options' => \Drupal::service('plugin.manager.flag.linktype')->getAllLinkTypes(), + // '#after_build' => array('flag_check_link_types'), + '#default_value' => $flag->getLinkTypePlugin()->getPluginId(), + // Give this a high weight so additions by the flag classes for entity- + // specific options go above. + '#weight' => 18, + '#attributes' => [ + 'class' => ['flag-link-options'], + ], + '#limit_validation_errors' => [['link_type']], + '#submit' => ['::submitSelectPlugin'], + '#required' => TRUE, + '#executes_submit_callback' => TRUE, + '#ajax' => [ + 'callback' => '::updateSelectedPluginType', + 'wrapper' => 'link-type-settings-wrapper', + 'event' => 'change', + 'method' => 'replace', + ], + ]; + $form['display']['link_type_submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Update'), + '#submit' => ['::submitSelectPlugin'], + '#weight' => 20, + '#attributes' => ['class' => ['js-hide']], + ]; + // Add the descriptions to each ratio button element. These attach to the + // elements when FormAPI expands them. + $action_link_plugin_defs = \Drupal::service('plugin.manager.flag.linktype')->getDefinitions(); + foreach ($action_link_plugin_defs as $key => $info) { + $form['display']['link_type'][$key]['#description'] = $info['description']; + } + + $action_link_plugin = $flag->getLinkTypePlugin(); + $form = $action_link_plugin->buildConfigurationForm($form, $form_state); + + return $form; + } + + /** + * Handles switching the configuration type selector. + */ + public function updateSelectedPluginType($form, FormStateInterface $form_state) { + // Rebuild the entity using the form's new state. + $this->entity = $this->buildEntity($form, $form_state); + $flag = $this->entity; + + // Reset the wrapper for the link type settings. + $form['display']['settings'] = [ + '#type' => 'container', + '#prefix' => '', + '#weight' => 21, + ]; + + // Update the link type settings wrapper for the new selection. + $action_link_plugin = $flag->getLinkTypePlugin(); + $form = $action_link_plugin->buildConfigurationForm($form, $form_state); + + $form_state->setRebuild(); + + return $form['display']['settings']; + } + + /** + * Handles submit call when sensor type is selected. + */ + public function submitSelectPlugin(array $form, FormStateInterface $form_state) { + $form_state->setRebuild(); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::validate(). + */ + public function validate(array $form, FormStateInterface $form_state) { + parent::validate($form, $form_state); + + // @todo Move this to the validation method for the confirm form plugin + $flag = $this->entity; + $flag->getFlagTypePlugin()->validateConfigurationForm($form, $form_state); + $flag->getLinkTypePlugin()->validateConfigurationForm($form, $form_state); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::save(). + */ + public function save(array $form, FormStateInterface $form_state) { + $flag = $this->entity; + + $flag->getFlagTypePlugin()->submitConfigurationForm($form, $form_state); + $flag->getLinkTypePlugin()->submitConfigurationForm($form, $form_state); + + $flag->enable(); + $status = $flag->save(); + $url = $flag->urlInfo(); + if ($status == SAVED_UPDATED) { + drupal_set_message(t('Flag %label has been updated.', ['%label' => $flag->label()])); + $this->logger('flag')->notice('Flag %label has been updated.', ['%label' => $flag->label(), 'link' => \Drupal::l(t('Edit'), $url)]); + } + else { + drupal_set_message(t('Flag %label has been added.', ['%label' => $flag->label()])); + $this->logger('flag')->notice('Flag %label has been added.', ['%label' => $flag->label(), 'link' => \Drupal::l(t('Edit'), $url)]); + } + + // We clear caches more vigorously if the flag was new. + // _flag_clear_cache($flag->entity_type, !empty($flag->is_new)); + + // Save permissions. + // This needs to be done after the flag cache has been cleared, so that + // the new permissions are picked up by hook_permission(). + // This may need to move to the flag class when we implement extra + // permissions for different flag types: http://drupal.org/node/879988 + + // If the flag ID has changed, clean up all the obsolete permissions. + if ($flag->id != $form['#flag_name']) { + $old_name = $form['#flag_name']; + $permissions = ["flag $old_name", "unflag $old_name"]; + foreach (array_keys(user_roles()) as $rid) { + user_role_revoke_permissions($rid, $permissions); + } + } + /* + foreach (array_keys(user_roles(!\Drupal::moduleHandler()->moduleExists('session_api'))) as $rid) { + // Create an array of permissions. + $permissions = array( + "flag $flag->name" => $flag->roles['flag'][$rid], + "unflag $flag->name" => $flag->roles['unflag'][$rid], + ); + user_role_change_permissions($rid, $permissions); + } + */ + // @todo: when we add database caching for flags we'll have to clear the + // cache again here. + + $form_state->setRedirect('flag.list'); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::delete(). + */ + public function delete(array $form, FormStateInterface $form_state) { + $form_state->setRedirect('flag_list'); + } + + /** + * Determines if the flag already exists. + * + * @param string $id + * The flag ID + * + * @return bool + * TRUE if the flag exists, FALSE otherwise. + */ + public function exists($id) { + // @todo: Make this injected like ActionFormBase::exists(). + return \Drupal::entityManager()->getStorage('flag')->load($id); + } + +} diff --git a/src/Form/FlaggingConfirmForm.php b/src/Form/FlaggingConfirmForm.php new file mode 100644 index 0000000..8e3807f --- /dev/null +++ b/src/Form/FlaggingConfirmForm.php @@ -0,0 +1,123 @@ +flag = $flag_service->getFlagByID($flag_id); + $this->entity = $flag_service->getFlaggableById($this->flag, $entity_id); + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'flag_flagging_confirm_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + $link_type = $this->flag->getLinkTypePlugin(); + + if ($this->isFlagged()) { + return $link_type->getUnflagQuestion(); + } + + return $link_type->getFlagQuestion(); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $destination = \Drupal::request()->get('destination'); + if (!empty($destination)) { + return Url::createFromPath($destination); + } + + return $this->entity->urlInfo(); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + if ($this->isFlagged()) { + return $this->flag->getUnflagLongText(); + } + + return $this->flag->getFlagLongText(); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + if ($this->isFlagged()) { + return $this->t('Unflag'); + } + + return $this->t('Flag'); + } + + /** + * Helper method to determine if the entity has been flagged or not. + * + * @return bool + * TRUE if the current entity is flagged, FALSE otherwise. + */ + protected function isFlagged() { + return $this->flag->isFlagged($this->entity); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + if ($this->isFlagged()) { + \Drupal::service('flag')->unflagByObject($this->flag, $this->entity); + } + else { + \Drupal::service('flag')->flagByObject($this->flag, $this->entity); + } + } + +} diff --git a/src/Form/FlaggingForm.php b/src/Form/FlaggingForm.php new file mode 100644 index 0000000..23954ab --- /dev/null +++ b/src/Form/FlaggingForm.php @@ -0,0 +1,67 @@ +t('Update Flagging'); + + // Customize the delete link. + if (isset($actions['delete'])) { + // @todo Why does the access call always fail? + unset($actions['delete']['#access']); + + $actions['delete']['#title'] = $this->t('Delete Flagging'); + + // Build the delete url from route. We need to build this manually + // otherwise Drupal will try to build the flagging entity's delete-form + // link. Since that route doesn't use the flagging ID, Drupal can't build + // the link for us. + $route_params = [ + 'flag_id' => $this->entity->getFlagId(), + 'entity_id' => $this->entity->getFlaggableId(), + 'destination' => \Drupal::request()->get('destination'), + ]; + $url = Url::fromRoute('flag.confirm_unflag', $route_params); + + $actions['delete']['#url'] = $url; + } + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $entity = $this->entity; + $entity->save(); + } +} diff --git a/src/Plugin/ActionLink/AJAXactionLink.php b/src/Plugin/ActionLink/AJAXactionLink.php new file mode 100644 index 0000000..814358c --- /dev/null +++ b/src/Plugin/ActionLink/AJAXactionLink.php @@ -0,0 +1,49 @@ + 'Flag this content?', + 'unflag_confirmation' => 'Unflag this content?', + ]; + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + $form['display']['settings']['link_options_confirm'] = [ + '#type' => 'fieldset', + '#title' => t('Options for the "Confirmation form" link type'), + // Any "link type" provider module must put its settings fields inside + // a fieldset whose HTML ID is link-options-LINKTYPE, where LINKTYPE is + // the machine-name of the link type. This is necessary for the + // radiobutton's JavaScript dependency feature to work. + '#id' => 'link-options-confirm', + ]; + + $form['display']['settings']['link_options_confirm']['flag_confirmation'] = [ + '#type' => 'textfield', + '#title' => t('Flag confirmation message'), + '#default_value' => $this->getFlagQuestion(), + '#description' => t('Message displayed if the user has clicked the "flag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to flag this content?"'), + // This will get changed to a state by flag_link_type_options_states(). + '#required' => TRUE, + ]; + + $form['display']['settings']['link_options_confirm']['unflag_confirmation'] = [ + '#type' => 'textfield', + '#title' => t('Unflag confirmation message'), + '#default_value' => $this->getUnflagQuestion(), + '#description' => t('Message displayed if the user has clicked the "unflag this" link and confirmation is required. Usually presented in the form of a question such as, "Are you sure you want to unflag this content?"'), + // This will get changed to a state by flag_link_type_options_states(). + '#required' => TRUE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + + if ($form_values['link_type'] == 'confirm') { + if (empty($form_values['flag_confirmation'])) { + $form_state->setErrorByName('flag_confirmation', 'A flag confirmation message is required when using the confirmation link type.'); + } + if (empty($form_values['unflag_confirmation'])) { + $form_state->setErrorByName('unflag_confirmation', 'An unflag confirmation message is required when using the confirmation link type.'); + } + } + + if (!preg_match('/^[a-z_][a-z0-9_]*$/', $form_values['id'])) { + $form_state->setErrorByName('label', 'The flag name may only contain lowercase letters, underscores, and numbers.'); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['flag_confirmation'] = $form_state->getValue('flag_confirmation'); + $this->configuration['unflag_confirmation'] = $form_state->getValue('unflag_confirmation'); + } + + /** + * Returns the flag confirm form question when flagging. + * + * @return string + * A string containing the flag question to display. + */ + public function getFlagQuestion() { + return $this->configuration['flag_confirmation']; + } + + /** + * Returns the flag confirm form question when unflagging. + * + * @return string + * A string containing the unflag question to display. + */ + public function getUnflagQuestion() { + return $this->configuration['unflag_confirmation']; + } + +} diff --git a/src/Plugin/ActionLink/FieldEntry.php b/src/Plugin/ActionLink/FieldEntry.php new file mode 100644 index 0000000..f2761ab --- /dev/null +++ b/src/Plugin/ActionLink/FieldEntry.php @@ -0,0 +1,163 @@ + 'fieldset', + '#title' => t('Options for the "Field entry" link type'), + // Any "link type" provider module must put its settings fields inside + // a fieldset whose HTML ID is link-options-LINKTYPE, where LINKTYPE is + // the machine-name of the link type. This is necessary for the + // radiobutton's JavaScript dependency feature to work. + '#id' => 'link-options-field_entry', + ]; + + $form['display']['settings']['link_options_field']['flag_confirmation'] = [ + '#type' => 'textfield', + '#title' => t('Flag confirmation message'), + '#default_value' => $this->configuration['flag_confirmation'], + '#description' => t('Message displayed if the user has clicked the "flag this" link and field entry is required. Usually presented in the form such as, "Please enter the flagging details."'), + // This will get changed to a state by flag_link_type_options_states(). + '#required' => TRUE, + ]; + + $form['display']['settings']['link_options_field']['flagging_edit_title'] = [ + '#type' => 'textfield', + '#title' => t('Enter flagging details message'), + '#default_value' => $this->configuration['edit_flagging'], + '#description' => t('Message displayed if the user has clicked the "Edit flag" link. Usually presented in the form such as, "Please enter the flagging details."'), + // This will get changed to a state by flag_link_type_options_states(). + '#required' => TRUE, + ]; + + $form['display']['settings']['link_options_field']['unflag_confirmation'] = [ + '#type' => 'textfield', + '#title' => t('Unflag confirmation message'), + '#default_value' => $this->configuration['unflag_confirmation'], + '#description' => t('Message displayed if the user has clicked the "delete flag" link in the field entry form. Usually presented in the form of a question such as, "Are you sure you want to unflag this content?"'), + // This will get changed to a state by flag_link_type_options_states(). + '#required' => TRUE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + + if ($form_state->getValue('link_type') == 'field_entry') { + if (empty($form_values['flag_confirmation'])) { + $form_state->setErrorByName('flag_confirmation', 'A flag confirmation message is required when using the field entry link type.'); + } + if (empty($form_values['flagging_edit_title'])) { + $form_state->setErrorByName('flagging_edit_title', 'An edit flagging details message is required when using the field entry link type.'); + } + if (empty($form_values['unflag_confirmation'])) { + $form_state->setErrorByName('unflag_confirmation', 'An unflag confirmation message is required when using the field entry link type.'); + } + } + + if (!preg_match('/^[a-z_][a-z0-9_]*$/', $form_state->getValue('id'))) { + $form_state->setErrorByName('label', 'The flag name may only contain lowercase letters, underscores, and numbers.'); + } + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + $this->configuration['flag_confirmation'] = $form_state->getValue('flag_confirmation'); + $this->configuration['edit_flagging'] = $form_state->getValue('flagging_edit_title'); + $this->configuration['unflag_confirmation'] = $form_state->getValue('unflag_confirmation'); + } + + /** + * Returns the flag confirm form question when flagging. + * + * We're copying the confirm form link type interface here so we can take + * advantage of the existing confirm form code without duplicating the class. + * + * @return string + * A string containing the flag question to display. + */ + public function getFlagQuestion() { + return $this->configuration['flag_confirmation']; + } + + /** + * Returns the edit flagging details form title. + * + * @return string + * A string containing the edit flagging details title to display. + */ + public function getEditFlaggingTitle() { + return $this->configuration['edit_flagging']; + } + + /** + * Returns the flag confirm form question when unflagging. + * + * We're copying the confirm form link type interface here so we can take + * advantage of the existing confirm form code without duplicating the class. + * + * @return string + * A string containing the unflag question to display. + */ + public function getUnflagQuestion() { + return $this->configuration['unflag_confirmation']; + } +} diff --git a/src/Plugin/ActionLink/Reload.php b/src/Plugin/ActionLink/Reload.php new file mode 100644 index 0000000..d3ec551 --- /dev/null +++ b/src/Plugin/ActionLink/Reload.php @@ -0,0 +1,34 @@ +getDefinitions() as $entity_id => $entity_type) { + if (in_array($entity_id, $this->ignoredEntities)) { + continue; + } + $derivatives[$entity_id] = [ + 'title' => $entity_type->getLabel(), + 'entity_type' => $entity_id, + ] + $base_plugin_def; + } + + return $derivatives; + } +} diff --git a/src/Plugin/Flag/CommentFlagType.php b/src/Plugin/Flag/CommentFlagType.php new file mode 100644 index 0000000..efc171c --- /dev/null +++ b/src/Plugin/Flag/CommentFlagType.php @@ -0,0 +1,104 @@ + '', + ]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + + $form = parent::buildConfigurationForm($form, $form_state); + + /* Options form extras for comment flags. */ + + $form['access']['access_author'] = [ + '#type' => 'radios', + '#title' => t('Flag access by content authorship'), + '#options' => [ + '' => t('No additional restrictions'), + 'comment_own' => t('Users may only flag own comments'), + 'comment_others' => t('Users may only flag comments by others'), + 'node_own' => t('Users may only flag comments of nodes they own'), + 'node_others' => t('Users may only flag comments of nodes by others'), + ], + '#default_value' => $this->configuration['access_author'], + '#description' => t("Restrict access to this flag based on the user's ownership of the content. Users must also have access to the flag through the role settings."), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['access_author'] = $form_state->getValue('access_author'); + } + + /** + * {@inheritdoc} + */ + public function typeAccessMultiple(array $entity_ids, AccountInterface $account) { + $access = []; + + // If all subtypes are allowed, we have nothing to say here. + if (empty($this->types)) { + return $access; + } + + // Ensure node types are granted access. This avoids a + // node_load() on every type, usually done by applies_to_entity_id(). + $query = db_select('comment', 'c'); + $query->innerJoin('node', 'n', 'c.nid = n.nid'); + $result = $query + ->fields('c', ['cid']) + ->condition('c.cid', $entity_ids, 'IN') + ->condition('n.type', $this->types, 'NOT IN') + ->execute(); + foreach ($result as $row) { + $access[$row->nid] = FALSE; + } + + return $access; + } + + /** + * {@inheritdoc} + */ + public function getAccessAuthorSetting() { + return $this->configuration['access_author']; + } +} diff --git a/src/Plugin/Flag/EntityFlagType.php b/src/Plugin/Flag/EntityFlagType.php new file mode 100644 index 0000000..28f67ff --- /dev/null +++ b/src/Plugin/Flag/EntityFlagType.php @@ -0,0 +1,217 @@ +entity_type = $plugin_definition['entity_type']; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $options = parent::defaultConfiguration(); + $options += [ + // Output the flag in the entity links. + // This is empty for now and will get overriden for different + // entities. + // @see hook_entity_view(). + 'show_in_links' => [], + // Output the flag as individual pseudofields. + 'show_as_field' => TRUE, + // Add a checkbox for the flag in the entity form. + // @see hook_field_attach_form(). + 'show_on_form' => FALSE, + 'show_contextual_link' => FALSE, + ]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + + /* Options form extras for the generic entity flag. */ + + // Add checkboxes to show flag link on each entity view mode. + $options = []; + $defaults = []; + $view_modes = \Drupal::entityManager()->getViewModes($this->entity_type); + foreach ($view_modes as $name => $view_mode) { + $options[$name] = t('Display on @name view mode', ['@name' => $view_mode['label']]); + $defaults[$name] = $this->showInLinks($name); + } + + $form['display']['show_in_links'] = [ + '#type' => 'checkboxes', + '#title' => t('Display in entity links'), + '#description' => t('Show the flag link with the other links on the entity.'), + '#options' => $options, + '#default_value' => $defaults, + ]; + + $form['display']['show_as_field'] = [ + '#type' => 'checkbox', + '#title' => t('Display link as field'), + '#description' => t('Show the flag link as a pseudofield, which can be ordered among other entity elements in the "Manage display" settings for the entity type.'), + '#default_value' => $this->showAsField(), + ]; + /* + if (empty($entity_info['fieldable'])) { + $form['display']['show_as_field']['#disabled'] = TRUE; + $form['display']['show_as_field']['#description'] = t("This entity type is not fieldable."); + } + */ + $form['display']['show_on_form'] = [ + '#type' => 'checkbox', + '#title' => t('Display checkbox on entity edit form'), + '#default_value' => $this->showOnForm(), + '#weight' => 5, + ]; + + // We use FieldAPI to put the flag checkbox on the entity form, so therefore + // require the entity to be fielable. Since this is a potential DX + // headscratcher for a developer wondering where this option has gone, + // we disable it and explain why. + /* + if (empty($entity_info['fieldable'])) { + $form['display']['show_on_form']['#disabled'] = TRUE; + $form['display']['show_on_form']['#description'] = t('This is only possible on entities which are fieldable.'); + } + */ + $form['display']['show_contextual_link'] = [ + '#type' => 'checkbox', + '#title' => t('Display in contextual links'), + '#default_value' => $this->showContextualLink(), + '#description' => t('Note that not all entity types support contextual links.'), + '#access' => \Drupal::moduleHandler()->moduleExists('contextual'), + '#weight' => 10, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + $form_values = $form_state->getValues(); + + // Check each of the display modes for the show_in_links field. + foreach ($form_values['show_in_links'] as $link_display) { + if (!empty($link_display)) { + return; + } + } + + // Check if the user selected display as a psudofield. + if (!empty($form_values['show_as_field'])) { + return; + } + + // Check if the user selected display on the entity edit form. + if (!empty($form_values['show_on_form'])) { + return; + } + + // Check if the user selected display as a contextual link. + if (!empty($form_values['show_contextual_link'])) { + return; + } + + // If we're still here, no display was selected. Return a form error. + $form_state->setErrorByName('show_as_field', 'No entity link display selected. Please select at least one link display such as \'Display link as field\'.'); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['show_in_links'] = $form_state->getValue('show_in_links'); + $this->configuration['show_as_field'] = $form_state->getValue('show_as_field'); + $this->configuration['show_on_form'] = $form_state->getValue('show_on_form'); + $this->configuration['show_contextual_link'] = $form_state->getValue('show_contextual_link'); + } + + /** + * Return the show in links setting given a view mode. + * + * @param string $name + * The name of the view mode. + * + * @return mixed + * The name of the view mode if the flag appears in links, 0 otherwise. + */ + public function showInLinks($name) { + if (!empty($this->configuration['show_in_links'][$name])) { + return $name; + } + + return 0; + } + + /** + * Returns the show as field setting. + * + * @return bool + * TRUE if the flag should appear as a psudofield, FALSE otherwise. + */ + public function showAsField() { + return $this->configuration['show_as_field']; + } + + /** + * Returns the show on form setting. + * + * @return bool + * TRUE if the flag should appear on the entity form, FALSE otherwise. + */ + public function showOnForm() { + return $this->configuration['show_on_form']; + } + + /** + * Returns the show on contextual link setting. + * + * @return bool + * TRUE if the flag should appear in contextual links, FALSE otherwise. + */ + public function showContextualLink() { + return $this->configuration['show_contextual_link']; + } +} diff --git a/src/Plugin/Flag/NodeFlagType.php b/src/Plugin/Flag/NodeFlagType.php new file mode 100644 index 0000000..bbc6d25 --- /dev/null +++ b/src/Plugin/Flag/NodeFlagType.php @@ -0,0 +1,134 @@ + 0, + 'access_author' => '', + ]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + /* Options form extras for node flags. */ + + $form['access']['access_author'] = [ + '#type' => 'radios', + '#title' => t('Flag access by content authorship'), + '#options' => [ + '' => t('No additional restrictions'), + 'own' => t('Users may only flag content they own'), + 'others' => t('Users may only flag content of others'), + ], + '#default_value' => $this->getAccessAuthorSetting(), + '#description' => t("Restrict access to this flag based on the user's ownership of the content. Users must also have access to the flag through the role settings."), + ]; + + // Support for i18n flagging requires Translation helpers module. + $form['i18n'] = [ + '#type' => 'radios', + '#title' => t('Internationalization'), + '#options' => [ + '1' => t('Flag translations of content as a group'), + '0' => t('Flag each translation of content separately'), + ], + '#default_value' => $this->getInternationalizationSetting(), + '#description' => t('Flagging translations as a group effectively allows users to flag the original piece of content regardless of the translation they are viewing. Changing this setting will not update content that has been flagged already.'), + '#access' => \Drupal::moduleHandler()->moduleExists('translation_helpers'), + '#weight' => 5, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['access_author'] = $form_state->getValue('access_author'); + $this->configuration['i18n'] = $form_state->getValue('i18n'); + } + + /** + * {@inheritdoc} + */ + public function typeAccessMultiple(array $entity_ids, AccountInterface $account) { + $access = []; + + // If all subtypes are allowed, we have nothing to say here. + if (empty($this->types)) { + return $access; + } + + // Ensure that only flaggable node types are granted access. This avoids a + // node_load() on every type, usually done by applies_to_entity_id(). + $result = db_select('node', 'n')->fields('n', ['nid']) + ->condition('nid', array_keys($entity_ids), 'IN') + ->condition('type', $this->types, 'NOT IN') + ->execute(); + foreach ($result as $row) { + $access[$row->nid] = FALSE; + } + + return $access; + } + + /** + * Returns the flag type access author setting. + * + * @return string + * The access author setting can be one of three values: + * - '' = No additional restrictions. + * - 'own' = Users may only flag content they own. + * - 'others' = Users may only flag content of others. + */ + public function getAccessAuthorSetting() { + return $this->configuration['access_author']; + } + + /** + * Returns the internationalization setting for the flag type. + * + * @return int + * The internationalization setting can be one of two values: + * - 1 = Flag translations of content as a group. + * - 0 = Flag each translation of content separately. + */ + public function getInternationalizationSetting() { + return $this->configuration['i18n']; + } +} diff --git a/src/Plugin/Flag/UserFlagType.php b/src/Plugin/Flag/UserFlagType.php new file mode 100644 index 0000000..7a20549 --- /dev/null +++ b/src/Plugin/Flag/UserFlagType.php @@ -0,0 +1,118 @@ + TRUE, + 'access_uid' => '', + ]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + /* Options form extras for user flags */ + + $form['access']['types'] = [ + // A user flag doesn't support node types. + // TODO: Maybe support roles instead of node types. + '#type' => 'value', + '#value' => array(0 => 0), + ]; + $form['access']['access_uid'] = [ + '#type' => 'checkbox', + '#title' => t('Users may flag themselves'), + '#description' => t('Disabling this option may be useful when setting up a "friend" flag, when a user flagging themselves does not make sense.'), + '#default_value' => $this->getAccessUidSetting() ? 0 : 1, + ]; + $form['display']['show_on_profile'] = [ + '#type' => 'checkbox', + '#title' => t('Display link on user profile page'), + '#description' => t('Show the link formatted as a user profile element.'), + '#default_value' => $this->showOnProfile(), + // Put this above 'show on entity'. + '#weight' => -1, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['access_uid'] = $form_state->getValue(['access', 'access_uid']); + $this->configuration['show_on_profile'] = $form_state->getValue(['display', 'show_on_profile']); + } + + /** + * {@inheritdoc} + */ + public function typeAccessMultiple(array $entity_ids, AccountInterface $account) { + $access = []; + + // Exclude anonymous. + if (array_key_exists(0, $entity_ids)) { + $access[0] = FALSE; + } + + // Prevent users from flagging themselves. + if ($this->access_uid == 'others' && array_key_exists($account->uid, $entity_ids)) { + $access[$account->uid] = FALSE; + } + + return $access; + } + + /** + * Specifies if users are able to flag themselves. + * + * @return bool|mixed + * TRUE if users are able to flag themselves, FALSE otherwise. + */ + public function getAccessUidSetting() { + return $this->configuration['access_uid']; + } + + /** + * Specifies if the flag link should appear on the user profile. + * + * @return bool + * TRUE if the flag link appears on the user profile, FALSE otherwise. + */ + public function showOnProfile() { + return $this->configuration['show_on_profile']; + } +} diff --git a/src/Plugin/views/argument/FlagViewsFlaggableArgument.php b/src/Plugin/views/argument/FlagViewsFlaggableArgument.php new file mode 100644 index 0000000..84773e1 --- /dev/null +++ b/src/Plugin/views/argument/FlagViewsFlaggableArgument.php @@ -0,0 +1,81 @@ +database = $database; + } + + /** + * Helper method to retrieve the flag entity from the views relationship. + * + * @return FlagInterface|null + * The flag entity selected in the relationship. + */ + public function getFlag() { + // When editing a view it's possible to delete the relationship (either by + // error or to later recreate it), so we have to guard against a missing + // one. + if (isset($this->view->relationship[$this->options['relationship']])) { + return $this->view->relationship[$this->options['relationship']]->getFlag(); + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + public function titleQuery() { + $titles = []; + + $flag = $this->getFlag(); + $entity_type = $flag->getFlaggableEntityType(); + + $def = \Drupal::entityManager()->getDefinition($entity_type); + $entity_keys = $def->getKeys(); + + $result = $this->database->select($def->getBaseTable(), 'o') + ->fields('o', $entity_keys['label']) + ->condition('o.' . $entity_keys['id'], $this->value, 'IN') + ->execute(); + + foreach ($result as $title) { + $titles[] = String::checkPlain($title->$entity_keys['label']); + } + + return $titles; + } + +} diff --git a/src/Plugin/views/field/FlagViewsFlaggedField.php b/src/Plugin/views/field/FlagViewsFlaggedField.php new file mode 100644 index 0000000..db222b5 --- /dev/null +++ b/src/Plugin/views/field/FlagViewsFlaggedField.php @@ -0,0 +1,59 @@ +formats['flag'] = [t('Flagged'), t('Not flagged')]; + // TODO: We could probably lift the '(Un)Flagged message' strings from the + // flag object, but a) we need to lift that from the relationship we're on + // and b) they will not necessarily make sense in a static context. + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['relationship'] = ['default' => 'flag_content_rel']; + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $form['relationship']['#default_value'] = $this->options['relationship']; + + parent::buildOptionsForm($form, $form_state); + } +} diff --git a/src/Plugin/views/field/FlagViewsLinkField.php b/src/Plugin/views/field/FlagViewsLinkField.php new file mode 100644 index 0000000..04894ec --- /dev/null +++ b/src/Plugin/views/field/FlagViewsLinkField.php @@ -0,0 +1,122 @@ +view->relationship[$this->options['relationship']])) { + return $this->view->relationship[$this->options['relationship']]->getFlag(); + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['text'] = [ + 'default' => '', + 'translatable' => TRUE, + ]; + + // Set the default relationship handler. The first instance of the + // FlagViewsRelationship should always have the id "flag_content_rel", so + // we set that as the default. + $options['relationship'] = array('default' => 'flag_content_rel'); + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $form['text'] = [ + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ]; + + $form['relationship']['#default_value'] = $this->options['relationship']; + + parent::buildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function query() { + // Intentionally do nothing here since we're only providing a link and not + // querying against a real table column. + } + + /** + * {@inheritdoc} + */ + public function render(ResultRow $values) { + // $entity = $this->getEntity($values); + return $this->renderLink($values->_entity, $values); + } + + /** + * Creates a render array for flag links. + * + * @param EntityInterface $entity + * The entity object. + * @param ResultRow $values + * The current result row. + * + * @return array + * The render array for the flag link. + */ + protected function renderLink(EntityInterface $entity, ResultRow $values) { + if (empty($entity)) { + return t('N/A'); + } + + $flag = $this->getFlag(); + $link_type_plugin = $flag->getLinkTypePlugin(); + $action = 'flag'; + + if ($flag->isFlagged($entity)) { + $action = 'unflag'; + } + + $link = $link_type_plugin->renderLink($action, $flag, $entity); + + return $link; + } + +} diff --git a/src/Plugin/views/filter/FlagViewsFilter.php b/src/Plugin/views/filter/FlagViewsFilter.php new file mode 100644 index 0000000..786cb10 --- /dev/null +++ b/src/Plugin/views/filter/FlagViewsFilter.php @@ -0,0 +1,63 @@ + 1); + $options['relationship'] = array('default' => 'flag_content_rel'); + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $form['value']['#type'] = 'radios'; + $form['value']['#title'] = t('Status'); + $form['value']['#options'] = [ + 1 => t('Flagged'), + 0 => t('Not flagged'), + // @todo Find out what in the hell filter type ALL is supposed to do. + // 'All' => t('All'), + ]; + $form['value']['#default_value'] = empty($this->options['value']) ? '0' : $this->options['value']; + $form['value']['#description'] = '

' . t('This filter is only needed if the relationship used has the "Include only flagged content" option unchecked. Otherwise, this filter is useless, because all records are already limited to flagged content.') . '

' . t('By choosing Not flagged, it is possible to create a list of content that is specifically not flagged.', array('@unflagged-url' => 'http://drupal.org/node/299335')) . '

'; + + $form['relationship']['#default_value'] = $this->options['relationship']; + + parent::buildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function query() { + $this->ensureMyTable(); + + $operator = $this->options['value'] ? 'IS NOT' : 'IS'; + $operator .= ' NULL'; + + $this->query->addWhere($this->options['group'], "$this->tableAlias.uid", NULL, $operator); + } +} diff --git a/src/Plugin/views/relationship/FlagViewsRelationship.php b/src/Plugin/views/relationship/FlagViewsRelationship.php new file mode 100644 index 0000000..62f1f0e --- /dev/null +++ b/src/Plugin/views/relationship/FlagViewsRelationship.php @@ -0,0 +1,128 @@ + NULL]; + $options['required'] = ['default' => 1]; + $options['user_scope'] = ['default' => 'current']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $entity_type = $this->definition['flaggable']; + $form['label']['#description'] .= ' ' . t('The name of the selected flag makes a good label.'); + + $flags = \Drupal::service('flag')->getFlags($entity_type); + + $default_value = $this->options['flag']; + if (!empty($flags)) { + $default_value = current(array_keys($flags)); + } + + $form['flag'] = [ + '#type' => 'radios', + '#title' => t('Flag'), + '#default_value' => $default_value, + '#required' => TRUE, + ]; + + foreach ($flags as $fid => $flag) { + if (!empty($flag)) { + $form['flag']['#options'][$fid] = $flag->label(); + } + } + + $form['user_scope'] = [ + '#type' => 'radios', + '#title' => t('By'), + '#options' => ['current' => t('Current user'), 'any' => t('Any user')], + '#default_value' => $this->options['user_scope'], + ]; + + $form['required']['#title'] = t('Include only flagged content'); + $form['required']['#description'] = t('If checked, only content that has this flag will be included. Leave unchecked to include all content; or, in combination with the Flagged filter, to limit the results to specifically unflagged content.', ['@unflagged-url' => 'http://drupal.org/node/299335']); + + if (!$form['flag']['#options']) { + $form = [ + 'error' => [ + '#markup' => '

' . t('No %type flags exist. You must first create a %type flag before being able to use this relationship type.', ['%type' => $entity_type, '@create-url' => Url::fromRoute('flag.list')->toString()]) . '

', + ], + ]; + } + } + + /** + * {@inheritdoc} + */ + public function query() { + if (!($flag = $this->getFlag())) { + return; + } + + $this->definition['extra'][] = [ + 'field' => 'fid', + 'value' => $flag->id, + 'numeric' => TRUE, + ]; + + if ($this->options['user_scope'] == 'current' && !$flag->isGlobal()) { + $this->definition['extra'][] = [ + 'field' => 'uid', + 'value' => '***CURRENT_USER***', + 'numeric' => TRUE, + ]; + $flag_roles = user_roles(FALSE, "flag $flag->label"); + if (isset($flag_roles[DRUPAL_ANONYMOUS_RID])) { + // Disable page caching for anonymous users. + \Drupal::service('page_cache_kill_switch')->trigger(); + + // Add in the SID from Session API for anonymous users. + $this->definition['extra'][] = [ + 'field' => 'sid', + 'value' => '***FLAG_CURRENT_USER_SID***', + 'numeric' => TRUE, + ]; + } + } + + // parent::query(); + } + + /** + * Get the flag of the relationship. + * + * @return \Drupal\flag\FlagInterface|null + * The flag being selected by in the view. + */ + public function getFlag() { + $flaggable = $this->definition['flaggable']; + $flag = \Drupal::service('flag')->getFlags($flaggable); + $this->options['flag'] = $flag; + return current($flag); + } +} diff --git a/src/Plugin/views/sort/FlagViewsSortFlagged.php b/src/Plugin/views/sort/FlagViewsSortFlagged.php new file mode 100644 index 0000000..186ee77 --- /dev/null +++ b/src/Plugin/views/sort/FlagViewsSortFlagged.php @@ -0,0 +1,52 @@ + t('Unflagged first'), + 'DESC' => t('Flagged first'), + ]; + } + + /** + * Display whether or not the sort order is ascending or descending. + */ + public function adminSummary() { + if (!empty($this->options['exposed'])) { + return t('Exposed'); + } + + // Get the labels defined in sortOptions(). + $sort_options = $this->sortOptions(); + return $sort_options[strtoupper($this->options['order'])]; + } + + /** + * {@inheritdoc} + */ + public function query() { + $this->ensureMyTable(); + + $this->query->addOrderBy(NULL, "$this->tableAlias.uid", $this->options['order']); + } +} diff --git a/src/Tests/FlagConfirmFormTest.php b/src/Tests/FlagConfirmFormTest.php new file mode 100644 index 0000000..edb6a24 --- /dev/null +++ b/src/Tests/FlagConfirmFormTest.php @@ -0,0 +1,146 @@ +adminUser = $this->drupalCreateUser([ + 'administer flags', + 'administer flagging display', + 'administer node display', + ]); + + $this->drupalLogin($this->adminUser); + $this->doCreateFlag(); + $this->doCreateNode(); + } + + /** + * Create a node type and a flag. + */ + public function doCreateFlag() { + // Create content type. + $this->drupalCreateContentType(['type' => $this->nodeType]); + + // Test with minimal value requirement. + $edit = [ + 'label' => $this->label, + 'id' => $this->id, + 'flag_entity_type' => 'flagtype_node', + 'flag_link_type' => 'confirm', + ]; + $this->drupalPostForm('admin/structure/flags/add', $edit, t('Continue')); + + // Check confirm form field entry. + $this->assertText(t('Flag confirmation message')); + $this->assertText(t('Unflag confirmation message')); + + // Update the flag. + $edit = [ + 'types[' . $this->nodeType . ']' => $this->nodeType, + 'flag_confirmation' => $this->flagConfirmMessage, + 'unflag_confirmation' => $this->unflagConfirmMessage, + ]; + $this->drupalPostForm(NULL, $edit, t('Create Flag')); + + // Check to see if the flag was created. + $this->assertText(t('Flag @this_label has been added.', ['@this_label' => $this->label])); + } + + /** + * Create a node and flag it. + */ + public function doCreateNode() { + $node = $this->drupalCreateNode(['type' => $this->nodeType]); + $node_id = $node->id(); + + // Grant the flag permissions to the authenticated role, so that both + // users have the same roles and share the render cache. + $role = Role::load(DRUPAL_AUTHENTICATED_RID); + $role->grantPermission('flag ' . $this->id); + $role->grantPermission('unflag ' . $this->id); + $role->save(); + + // Create and login a new user. + $user_1 = $this->drupalCreateUser(); + $this->drupalLogin($user_1); + + // Click the flag link. + $this->drupalGet('node/' . $node_id); + $this->clickLink(t('Flag this item')); + + // Check if we have the confirm form message displayed. + $this->assertText($this->flagConfirmMessage); + + // Submit the confirm form. + $this->drupalPostForm('flag/confirm/flag/' . $this->id . '/' . $node_id, [], t('Flag')); + $this->assertResponse(200); + + // Check that the node is flagged. + $this->drupalGet('node/' . $node_id); + $this->assertLink(t('Unflag this item')); + } + +} diff --git a/src/Tests/FlagEnableDisableTest.php b/src/Tests/FlagEnableDisableTest.php new file mode 100644 index 0000000..8f533a3 --- /dev/null +++ b/src/Tests/FlagEnableDisableTest.php @@ -0,0 +1,161 @@ +adminUser = $this->drupalCreateUser([ + 'administer flags', + 'administer flagging display', + 'administer node display', + ]); + + $this->drupalLogin($this->adminUser); + + $this->doCreateFlag(); + $this->doCreateNode(); + $this->doDisableFlag(); + $this->doEnableFlag(); + } + + /** + * Create a node type and a flag. + */ + public function doCreateFlag() { + // Create content type. + $this->drupalCreateContentType(['type' => $this->nodeType]); + + // Test with minimal value requirement. + $edit = [ + 'label' => $this->label, + 'id' => $this->id, + 'flag_entity_type' => 'flagtype_node', + ]; + $this->drupalPostForm('admin/structure/flags/add', $edit, t('Continue')); + + $edit = [ + 'types[' . $this->nodeType . ']' => $this->nodeType, + 'flag_short' => $this->flagShortText, + 'unflag_short' => $this->unflagShortText, + ]; + $this->drupalPostForm(NULL, $edit, t('Create Flag')); + } + + /** + * Create a node and flag it. + */ + public function doCreateNode() { + $node = $this->drupalCreateNode(['type' => $this->nodeType]); + $this->node_id = $node->id(); + + // Grant the flag permissions to the authenticated role, so that both + // users have the same roles and share the render cache. + $role = Role::load(DRUPAL_AUTHENTICATED_RID); + $role->grantPermission('flag ' . $this->id); + $role->grantPermission('unflag ' . $this->id); + $role->save(); + + // Click the flag link. + $this->drupalGet('node/' . $this->node_id); + + $this->assertText($this->flagShortText); + } + + /** + * Disable the flag and ensure the link does not appear on entities. + */ + public function doDisableFlag() { + $this->drupalGet('admin/structure/flags'); + $this->assertText(t('enabled')); + + $this->drupalPostForm('flag/disable/' . $this->id, [], t('Disable')); + $this->assertResponse(200); + + $this->drupalGet('admin/structure/flags'); + $this->assertText(t('disabled')); + + $this->drupalGet('node/' . $this->node_id); + $this->assertNoText($this->flagShortText); + } + + /** + * Enable the flag and ensure it appears on target entities. + */ + public function doEnableFlag() { + $this->drupalGet('admin/structure/flags'); + $this->assertText(t('disabled')); + + $this->drupalPostForm('flag/enable/' . $this->id, [], t('Enable')); + $this->assertResponse(200); + + $this->drupalGet('admin/structure/flags'); + $this->assertText(t('enabled')); + + $this->drupalGet('node/' . $this->node_id); + $this->assertText($this->flagShortText); + } +} diff --git a/src/Tests/FlagFieldEntryTest.php b/src/Tests/FlagFieldEntryTest.php new file mode 100644 index 0000000..49537aa --- /dev/null +++ b/src/Tests/FlagFieldEntryTest.php @@ -0,0 +1,206 @@ +adminUser = $this->drupalCreateUser([ + 'administer flags', + 'administer flagging display', + 'administer flagging fields', + 'administer node display', + ]); + + $this->drupalLogin($this->adminUser); + $this->doCreateFlag(); + $this->doAddFields(); + $this->doCreateFlagNode(); + $this->doEditFlagField(); + } + + /** + * Create a node type and flag. + */ + public function doCreateFlag() { + // Create content type. + $this->drupalCreateContentType(['type' => $this->nodeType]); + + // Test with minimal value requirement. + $edit = [ + 'label' => $this->label, + 'id' => $this->id, + 'flag_entity_type' => 'flagtype_node', + 'flag_link_type' => 'field_entry', + ]; + $this->drupalPostForm('admin/structure/flags/add', $edit, t('Continue')); + + // Check confirm form field entry. + $this->assertText(t('Flag confirmation message')); + $this->assertText(t('Enter flagging details message')); + $this->assertText(t('Unflag confirmation message')); + + // Update the flag. + $edit = [ + 'types[' . $this->nodeType . ']' => $this->nodeType, + 'flag_confirmation' => $this->flagConfirmMessage, + 'flagging_edit_title' => $this->flagDetailsMessage, + 'unflag_confirmation' => $this->unflagConfirmMessage, + ]; + $this->drupalPostForm(NULL, $edit, t('Create Flag')); + + // Check to see if the flag was created. + $this->assertText(t('Flag @this_label has been added.', ['@this_label' => $this->label])); + } + + /** + * Add fields to flag. + */ + public function doAddFields() { + $edit = [ + 'fields[_add_new_field][label]' => $this->flagFieldLabel, + 'fields[_add_new_field][field_name]' => $this->flagFieldId, + 'fields[_add_new_field][type]' => 'text', + ]; + $this->drupalPostForm('admin/structure/flags/manage/' . $this->id . '/fields', $edit, t('Save')); + + $edit = [ + 'field_storage[cardinality]' => '-1', + 'field_storage[cardinality_number]' => '1', + ]; + $this->drupalPostForm(NULL, $edit, t('Save field settings')); + + $this->assertText(t('Updated field') . ' ' . $this->flagFieldLabel . ' ' . t('field settings.')); + } + + /** + * Create a node and flag it. + */ + public function doCreateFlagNode() { + $node = $this->drupalCreateNode(['type' => $this->nodeType]); + $this->nodeId = $node->id(); + + // Grant the flag permissions to the authenticated role, so that both + // users have the same roles and share the render cache. + $role = Role::load(DRUPAL_AUTHENTICATED_RID); + $role->grantPermission('flag ' . $this->id); + $role->grantPermission('unflag ' . $this->id); + $role->save(); + + // Create and login a new user. + $user_1 = $this->drupalCreateUser(); + $this->drupalLogin($user_1); + + // Click the flag link. + $this->drupalGet('node/' . $this->nodeId); + $this->clickLink(t('Flag this item')); + + // Check if we have the confirm form message displayed. + $this->assertText($this->flagConfirmMessage); + + // Enter the field value and submit it. + $this->flagFieldValue = $this->randomString(); + $edit = [ + 'field_' . $this->flagFieldId . '[0][value]' => $this->flagFieldValue, + ]; + $this->drupalPostForm(NULL, $edit, t('Update Flagging')); + + // Check that the node is flagged. + $this->assertLink(t('Unflag this item')); + } + + /** + * Edit the field value of the existing flagging. + */ + public function doEditFlagField() { + // Get the details form. + $this->drupalGet('flag/details/edit/' . $this->id . '/' . $this->nodeId); + + // See if the details message is displayed. + $this->assertText($this->flagDetailsMessage); + + // See if the field value was preserved. + $this->assertFieldByName('field_' . $this->flagFieldId . '[0][value]', $this->flagFieldValue); + + // Update the field value. + $this->flagFieldValue = $this->randomString(); + $edit = [ + 'field_' . $this->flagFieldId . '[0][value]' => $this->flagFieldValue, + ]; + $this->drupalPostForm(NULL, $edit, t('Update Flagging')); + + // Get the details form. + $this->drupalGet('flag/details/edit/' . $this->id . '/' . $this->nodeId); + + // See if the field value was preserved. + $this->assertFieldByName('field_' . $this->flagFieldId . '[0][value]', $this->flagFieldValue); + } + +} diff --git a/src/Tests/FlagSimpleTest.php b/src/Tests/FlagSimpleTest.php new file mode 100644 index 0000000..2d99134 --- /dev/null +++ b/src/Tests/FlagSimpleTest.php @@ -0,0 +1,301 @@ +adminUser = $this->drupalCreateUser([ + 'administer flags', + 'administer flagging display', + 'administer node display', + ]); + + $this->drupalLogin($this->adminUser); + + $this->doTestFlagAdd(); + $this->doTestHideFlagLinkFromTeaser(); + } + + /** + * Flag creation. + */ + public function doTestFlagAdd() { + // Create content type. + $this->drupalCreateContentType(['type' => $this->nodeType]); + + // Test with minimal value requirement. + $edit = [ + 'label' => $this->label, + 'id' => $this->id, + ]; + $this->drupalPostForm('admin/structure/flags/add', $edit, t('Continue')); + // Check for fieldset titles. + $this->assertText(t('Messages')); + $this->assertText(t('Flag access')); + $this->assertText(t('Display options')); + + $edit = [ + 'types[' . $this->nodeType . ']' => $this->nodeType, + ]; + $this->drupalPostForm(NULL, $edit, t('Create Flag')); + + $this->assertText(t('Flag @this_label has been added.', ['@this_label' => $this->label])); + + // Continue test process. + $this->doTestCreateNodeAndFlagIt(); + } + + /** + * Node creation and flagging. + */ + public function doTestCreateNodeAndFlagIt() { + $node = $this->drupalCreateNode(['type' => $this->nodeType]); + $node_id = $node->id(); + + // Grant the flag permissions to the authenticated role, so that both + // users have the same roles and share the render cache. + $role = Role::load(DRUPAL_AUTHENTICATED_RID); + $role->grantPermission('flag ' . $this->id); + $role->grantPermission('unflag ' . $this->id); + $role->save(); + + // Create and login a new user. + $user_1 = $this->drupalCreateUser(); + $this->drupalLogin($user_1); + + $this->drupalGet('node/' . $node_id); + $this->clickLink('Flag this item'); + $this->assertResponse(200); + $this->assertLink('Unflag this item'); + + // Switch user to check flagging link. + $user_2 = $this->drupalCreateUser(); + $this->drupalLogin($user_2); + $this->drupalGet('node/' . $node_id); + $this->assertResponse(200); + $this->assertLink('Flag this item'); + + // Switch back to first user and unflag. + $this->drupalLogin($user_1); + $this->drupalGet('node/' . $node_id); + + $this->clickLink('Unflag this item'); + $this->assertResponse(200); + $this->assertLink('Flag this item'); + + // Check that the anonymous user, who does not have the necessary + // permissions, does not see the flag link. + $this->drupalLogout(); + $this->drupalGet('node/' . $node_id); + $this->assertNoLink('Flag this item'); + } + + /** + * Node creation and flag link. + */ + public function doTestHideFlagLinkFromTeaser() { + $this->drupalLogin($this->adminUser); + + $node = $this->drupalCreateNode([ + 'type' => $this->nodeType, + 'promote' => TRUE, + ]); + $node_id = $node->id(); + $node_title = $node->getTitle(); + + $this->drupalGet('node'); + $this->assertText($node_title); + $this->assertLink('Flag this item'); + + // Set flag format to hidden for teaser display and post form. + $edit = [ + 'fields[flag_' . $this->id . '][type]' => 'hidden', + ]; + + $this->drupalPostForm('admin/structure/types/manage/' . $this->nodeType . '/display/teaser', $edit, t('Save')); + + // Check if form is saved successfully. + $this->assertText('Your settings have been saved.'); + + $this->drupalGet('node'); + $this->assertText($node_title); + $this->assertNoLink('Flag this item'); + + $this->drupalGet('node/' . $node_id); + + } + + /** + * Creates user, sets flags and deletes user. + */ + public function doTestUserDeletion() { + $node = $this->drupalCreateNode(['type' => $this->nodeType]); + $node_id = $node->id(); + + // Create and login a new user. + $user_1 = $this->drupalCreateUser(); + $this->drupalLogin($user_1); + + $this->drupalGet('node/' . $node_id); + $this->clickLink('Flag this item'); + $this->assertResponse(200); + $this->assertLink('Unflag this item'); + + $count_flags_before = \Drupal::entityQuery('flagging') + ->condition('uid', $user_1->id()) + ->condition('fid', $this->id) + ->condition('entity_type', $node->getEntityTypeId()) + ->condition('entity_id', $node_id) + ->count() + ->execute(); + + $this->assertTrue(1, $count_flags_before); + + $user_1->delete(); + + $count_flags_after = \Drupal::entityQuery('flagging') + ->condition('uid', $user_1->id()) + ->condition('fid', $this->id) + ->condition('entity_type', $node->getEntityTypeId()) + ->condition('entity_id', $node_id) + ->count() + ->execute(); + + $this->assertTrue(0, $count_flags_after); + } + + /** + * Flags a node using different user accounts and checks flag counts. + */ + public function doTestFlagCounts() { + $node = $this->drupalCreateNode(['type' => $this->nodeType]); + $node_id = $node->id(); + + // Create and login user 1. + $user_1 = $this->drupalCreateUser(); + $this->drupalLogin($user_1); + + // Flag node (first count). + $this->drupalGet('node/' . $node_id); + $this->clickLink('Flag this item'); + $this->assertResponse(200); + $this->assertLink('Unflag this item'); + + // Check for 1 flag count. + $count_flags_before = \Drupal::entityQuery('flag_counts') + ->condition('fid', $this->id) + ->condition('entity_type', $node->getEntityTypeId()) + ->condition('entity_id', $node_id) + ->count() + ->execute(); + $this->assertTrue(1, $count_flags_before); + + // Logout user 1, create and login user 2. + $user_2 = $this->drupalCreateUser(); + $this->drupalLogin($user_2); + + // Flag node (second count). + $this->drupalGet('node/' . $node_id); + $this->clickLink('Flag this item'); + $this->assertResponse(200); + $this->assertLink('Unflag this item'); + + // Check for 2 flag counts. + $count_flags_after = \Drupal::entityQuery('flag_counts') + ->condition('fid', $this->id) + ->condition('entity_type', $node->getEntityTypeId()) + ->condition('entity_id', $node_id) + ->count() + ->execute(); + $this->assertTrue(2, $count_flags_after); + + // Unflag the node again. + $this->drupalGet('node/' . $node_id); + $this->clickLink('Unflag this item'); + $this->assertResponse(200); + $this->assertLink('Flag this item'); + + // Check for 1 flag count. + $count_flags_before = \Drupal::entityQuery('flag_counts') + ->condition('fid', $this->id) + ->condition('entity_type', $node->getEntityTypeId()) + ->condition('entity_id', $node_id) + ->count() + ->execute(); + $this->assertTrue(1, $count_flags_before); + + // Delete user 1. + $user_1->delete(); + + // Check for 0 flag counts, user deletion should lead to count decrement + // or row deletion. + $count_flags_before = \Drupal::entityQuery('flag_counts') + ->condition('fid', $this->id) + ->condition('entity_type', $node->getEntityTypeId()) + ->condition('entity_id', $node_id) + ->count() + ->execute(); + $this->assertTrue(0, $count_flags_before); + } +} diff --git a/tests/flag.test b/tests/flag.test deleted file mode 100644 index 461650e..0000000 --- a/tests/flag.test +++ /dev/null @@ -1,119 +0,0 @@ - t('Flag tests'), - 'description' => t('Add, edit and delete flags.'), - 'group' => t('Flag'), - ); - } - - /** - * Implementation of setUp(). - */ - function setUp() { - parent::setUp('flag'); - - // Create and login user - $admin_user = $this->drupalCreateUser(array('access administration pages', 'administer flags')); - $this->drupalLogin($admin_user); - } - - /** - * Create a flag through the UI and ensure that it is saved properly. - */ - function testFlagAdmin() { - // Add a new flag using the UI. - $edit = array( - 'name' => strtolower($this->randomName()), - 'title' => $this->randomName(), - 'flag_short' => 'flag short [nid]', - 'flag_long' => 'flag long [nid]', - 'flag_message' => 'flag message [nid]', - 'unflag_short' => 'unflag short [nid]', - 'unflag_long' => 'unflag long [nid]', - 'unflag_message' => 'unflag message [nid]', - 'roles[flag][2]' => TRUE, - 'roles[unflag][2]' => TRUE, - 'types[story]' => FALSE, - 'types[page]' => TRUE, - 'show_on_teaser' => FALSE, - 'show_on_page' => FALSE, - 'show_on_form' => FALSE, - 'link_type' => 'toggle', - ); - $saved = $edit; - $saved['roles'] = array('flag' => array(2), 'unflag' => array(2)); - $saved['types'] = array('page'); - unset($saved['roles[flag][2]'], $saved['roles[unflag][2]'], $saved['types[story]'], $saved['types[page]']); - - $this->drupalPost(FLAG_ADMIN_PATH . '/add/node/' . $edit['name'], $edit, t('Submit')); - - $flag = flag_get_flag($edit['name']); - - // Check that the flag object is in the database. - $this->assertTrue($flag != FALSE, t('Flag object found in database')); - - // Check each individual property of the flag and make sure it was set. - foreach ($saved as $property => $value) { - $this->assertEqual($flag->$property, $value, t('Flag property %property properly saved.', array('%property' => $property))); - } - - // Edit the flag through the UI. - $edit = array( - 'name' => strtolower($this->randomName()), - 'title' => $this->randomName(), - 'flag_short' => 'flag 2 short [nid]', - 'flag_long' => 'flag 2 long [nid]', - 'flag_message' => 'flag 2 message [nid]', - 'unflag_short' => 'unflag 2 short [nid]', - 'unflag_long' => 'unflag 2 long [nid]', - 'unflag_message' => 'unflag 2 message [nid]', - 'roles[flag][2]' => TRUE, - 'roles[unflag][2]' => TRUE, - 'types[story]' => TRUE, - 'types[page]' => FALSE, - 'show_on_teaser' => TRUE, - 'show_on_page' => TRUE, - 'show_on_form' => TRUE, - 'link_type' => 'normal', - ); - $saved = $edit; - $saved['roles'] = array('flag' => array(2), 'unflag' => array(2)); - $saved['types'] = array('story'); - unset($saved['roles[flag][2]'], $saved['roles[unflag][2]'], $saved['types[story]'], $saved['types[page]']); - - $this->drupalPost(FLAG_ADMIN_PATH . '/edit/' . $flag->name, $edit, t('Submit')); - - flag_get_flags(NULL, NULL, NULL, TRUE); - $flag = flag_get_flag($edit['name']); - - // Check that the flag object is in the database. - $this->assertTrue($flag != FALSE, t('Flag object found in database')); - - // Check each individual property of the flag and make sure it was set. - foreach ($saved as $property => $value) { - $this->assertEqual($flag->$property, $value, t('Flag property %property properly saved.', array('%property' => $property))); - } - - // Delete the flag through the UI. - $this->drupalPost(FLAG_ADMIN_PATH . '/delete/' . $flag->name, array(), t('Delete')); - flag_get_flags(NULL, NULL, NULL, TRUE); - $this->assertFalse(flag_get_flag($flag->name), t('Flag successfully deleted.')); - } - - /** - * Test that only allowed users have access to flags. - */ - function testFlagAccess() { - - } - -} - diff --git a/theme/README.txt b/theme/README.txt index d369781..ea98c51 100644 --- a/theme/README.txt +++ b/theme/README.txt @@ -17,11 +17,13 @@ In order to customize flag theming: - Edit that copy to your liking. -Template variants[3] +Template variants ----------------- -In addition, the theme layer will first look for the template -'flag--.tpl.php' before it turns to 'flag.tpl.php'. This too -you should place in your theme's folder.[2][1] +The theme layer will first look for the following templates in this order: + - flag--.tpl.php + - flag--.tpl.php + - flag.tpl.php +These should also be placed in your theme's folder.[2][1] Footnotes @@ -32,6 +34,3 @@ Footnotes file. This step is needed if you create or rename template files. This step *isn't* needed if you merely modify the contents of a file. Instructions on how to clear you theme registry are at http://drupal.org/node/173880#theme-registry - -[3] For template variants to work correctly you must use Drupal 6.3 or above (or -apply the patch from http://drupal.org/node/241570). diff --git a/theme/flag-admin.css b/theme/flag-admin.css index 2c6ac56..f7f1567 100644 --- a/theme/flag-admin.css +++ b/theme/flag-admin.css @@ -1,4 +1,3 @@ - table.flag-admin-table { width: auto; margin: 0; diff --git a/theme/flag-admin.js b/theme/flag-admin.js index 7867149..e52f2c4 100644 --- a/theme/flag-admin.js +++ b/theme/flag-admin.js @@ -1,3 +1,8 @@ +/** + * @file + * Contains flag admin javascript. + */ + (function ($) { /** @@ -40,52 +45,4 @@ Drupal.behaviors.flagRoles.attach = function(context) { } }; - -/** - * Behavior to make link options dependent on the link radio button. - */ -Drupal.behaviors.flagLinkOptions = {}; -Drupal.behaviors.flagLinkOptions.attach = function(context) { - $('.flag-link-options input.form-radio', context).change(function() { - // Reveal only the fieldset whose ID is link-options-LINKTYPE, - // where LINKTYPE is the value of the selected radio button. - var radioButton = this; - var $relevant = $('fieldset#link-options-' + radioButton.value); - var $irrelevant = $('fieldset[id^=link-options-]').not($relevant); - - $relevant.show(); - $irrelevant.hide(); - - if ($relevant.size()) { - $('#link-options-intro').show(); - } - else { - $('#link-options-intro').hide(); - } - }) - // Hide the link options by default if needed. - .filter(':checked').trigger('change'); -}; - -/** - * Vertical tabs integration. - */ -Drupal.behaviors.flagSummary = {}; - -Drupal.behaviors.flagSummary.attach = function (context) { - $('fieldset.flag-fieldset', context).drupalSetSummary(function(context) { - var flags = []; - $('input:checkbox:checked', context).each(function() { - flags.push(this.title); - }); - - if (flags.length) { - return flags.join(', '); - } - else { - return Drupal.t('No flags'); - } - }); -}; - })(jQuery); diff --git a/theme/flag.css b/theme/flag.css index 6668658..7f666eb 100644 --- a/theme/flag.css +++ b/theme/flag.css @@ -1,17 +1,34 @@ - .flag-message { position: absolute; top: 1.7em; line-height: normal; left: 0; + text-align: left; width: 300px; font-size: .8em; } +.flag-message.flag-failure-message { + border: 1px solid; + border-color: #ed5; + color: #840; + background-color: #fffce5; + padding: 2px; +} + .flag-wrapper { position: relative; } +/* Better contextual link support, prevent line wrapping. */ +ul.contextual-links li .flag-wrapper a { + display: inline-block; +} + +ul.contextual-links li .flag-wrapper { + display: block; +} + /* The rest deals with indicating the waiting state. */ .flag-waiting a { diff --git a/theme/flag.js b/theme/flag.js index 376ccdb..6588c1b 100644 --- a/theme/flag.js +++ b/theme/flag.js @@ -1,3 +1,8 @@ +/** + * @file + * Contains Flag module javascript. + */ + (function ($) { /** @@ -33,24 +38,22 @@ Drupal.flagLink = function(context) { // Find the wrapper of the old link. var $wrapper = $(element).parents('.flag-wrapper:first'); - if ($wrapper.length == 0) { - // If no ancestor wrapper was found, or if the 'flag-wrapper' class is - // attached to the element itself, then take the element itself. - $wrapper = $(element); - } // Replace the old link with the new one. $wrapper.after($newLink).remove(); Drupal.attachBehaviors($newLink.get(0)); $('.flag-message', $newLink).fadeIn(); - setTimeout(function(){ $('.flag-message', $newLink).fadeOut() }, 3000); + setTimeout(function(){ $('.flag-message.flag-auto-remove', $newLink).fadeOut() }, 3000); return $newLink.get(0); } /** * A click handler that is attached to all elements. */ - function flagClick() { + function flagClick(event) { + // Prevent the default browser click handler. + event.preventDefault(); + // 'this' won't point to the element when it's inside the ajax closures, // so we reference it using a variable. var element = this; @@ -68,34 +71,39 @@ Drupal.flagLink = function(context) { // Hide any other active messages. $('span.flag-message:visible').fadeOut(); - // Send POST request + // Send POST request. $.ajax({ type: 'POST', url: element.href, data: { js: true }, dataType: 'json', success: function (data) { - if (data.status) { - // Success. - data.link = $wrapper.get(0); - $.event.trigger('flagGlobalBeforeLinkUpdate', [data]); - if (!data.preventDefault) { // A handler may cancel updating the link. - data.link = updateLink(element, data.newLink); - } - $.event.trigger('flagGlobalAfterLinkUpdate', [data]); - } - else { - // Failure. - alert(data.errorMessage); - $wrapper.removeClass('flag-waiting'); + data.link = $wrapper.get(0); + $.event.trigger('flagGlobalBeforeLinkUpdate', [data]); + if (!data.preventDefault) { + // A handler may cancel updating the link. + data.link = updateLink(element, data.newLink); } + + // Find all the link wrappers on the page for this flag, but exclude + // the triggering element because Flag's own javascript updates it. + var $wrappers = $('.flag-wrapper.flag-' + data.flagName.flagNameToCSS() + '-' + data.contentId).not(data.link); + var $newLink = $(data.newLink); + + // Hide message, because we want the message to be shown on the triggering element alone. + $('.flag-message', $newLink).hide(); + + // Finally, update the page. + $wrappers = $newLink.replaceAll($wrappers); + Drupal.attachBehaviors($wrappers.parent()); + + $.event.trigger('flagGlobalAfterLinkUpdate', [data]); }, error: function (xmlhttp) { - alert('An HTTP error '+ xmlhttp.status +' occurred.\n'+ element.href); + alert('An HTTP error ' + xmlhttp.status + ' occurred.\n' + element.href); $wrapper.removeClass('flag-waiting'); } }); - return false; } $('a.flag-link-toggle:not(.flag-processed)', context).addClass('flag-processed').click(flagClick); @@ -109,18 +117,17 @@ Drupal.flagAnonymousLinks = function(context) { this.href += (this.href.match(/\?/) ? '&' : '?') + 'has_js=1'; $(this).addClass('flag-anonymous-processed'); }); -} +}; String.prototype.flagNameToCSS = function() { return this.replace(/_/g, '-'); -} +}; /** * A behavior specifically for anonymous users. Update links to the proper state. */ Drupal.flagAnonymousLinkTemplates = function(context) { // Swap in current links. Cookies are set by PHP's setcookie() upon flagging. - var templates = Drupal.settings.flag.templates; // Build a list of user-flags. @@ -159,7 +166,7 @@ Drupal.flagAnonymousLinkTemplates = function(context) { } } } -} +}; /** * Utility function used to set Flag cookies. @@ -168,11 +175,14 @@ Drupal.flagAnonymousLinkTemplates = function(context) { * Written by Klaus Hartl. */ Drupal.flagCookie = function(name, value, options) { - if (typeof value != 'undefined') { // name and value given, set cookie + if (typeof value != 'undefined') { + // Name and value given, set cookie. options = options || {}; if (value === null) { value = ''; - options = $.extend({}, options); // clone object since it's unexpected behavior if the expired property were changed + // Clone object since it's unexpected behavior if the expired property + // were changed. + options = $.extend({}, options); options.expires = -1; } var expires = ''; @@ -184,7 +194,9 @@ Drupal.flagCookie = function(name, value, options) { } else { date = options.expires; } - expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE + + // Use expires attribute, max-age is not supported by IE. + expires = '; expires=' + date.toUTCString(); } // NOTE Needed to parenthesize options.path and options.domain // in the following expressions, otherwise they evaluate to undefined @@ -193,7 +205,8 @@ Drupal.flagCookie = function(name, value, options) { var domain = options.domain ? '; domain=' + (options.domain) : ''; var secure = options.secure ? '; secure' : ''; document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); - } else { // only name given, get cookie + } else { + // Only name given, get cookie. var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); diff --git a/theme/flag.tpl.php b/theme/flag.tpl.php index fadce0b..64455f9 100644 --- a/theme/flag.tpl.php +++ b/theme/flag.tpl.php @@ -2,34 +2,48 @@ /** * @file - * Default theme implementation to display a flag link, and a message after the action - * is carried out. + * Default theme implementation to display a flag link. * * Available variables: * * - $flag: The flag object itself. You will only need to use it when the * following variables don't suffice. - * - $flag_name_css: The flag name, with all "_" replaced with "-". For use in 'class' - * attributes. - * - $flag_classes: A space-separated list of CSS classes that should be applied to the link. + * - $flag_name_css: The flag name, with all "_" replaced with "-". For use in + * 'class' attributes. + * - $flag_classes: A space-separated list of CSS classes that should be + * applied to the link. * - * - $action: The action the link is about to carry out, either "flag" or "unflag". + * - $action: The action the link is about to carry out, either "flag" or + * "unflag". * - $status: The status of the item; either "flagged" or "unflagged". * * - $link_href: The URL for the flag link. * - $link_text: The text to show for the link. * - $link_title: The title attribute for the link. * - * - $message_text: The long message to show after a flag action has been carried out. - * - $after_flagging: This template is called for the link both before and after being - * flagged. If displaying to the user immediately after flagging, this value - * will be boolean TRUE. This is usually used in conjunction with immedate - * JavaScript-based toggling of flags. + * - $message_text: The long message to show after a flag action has been + * carried out. + * - $message_classes: A space-separated list of CSS classes that should be + * applied to the message. + * - $after_flagging: This template is called for the link both before and + * after being flagged. If displaying to the user immediately after flagging, + * this value will be boolean TRUE. This is usually used in conjunction with + * immediate JavaScript-based toggling of flags. + * - $needs_wrapping_element: Determines whether the flag displays a wrapping + * HTML DIV element. * - * NOTE: This template spaces out the tags for clarity only. When doing some - * advanced theming you may have to remove all the whitespace. + * Template suggestions available, listed from the most specific template to + * the least. Drupal will use the most specific template it finds: + * - flag--name.tpl.php + * - flag--link-type.tpl.php + * + * NOTE: This template spaces out the tags for clarity only. When doing + * some advanced theming you may have to remove all the whitespace. */ ?> + +
+   @@ -37,8 +51,11 @@ - + + +
+