diff --git a/composer.json b/composer.json
index 3f5d688105a34913bcae58ce1732f8e16ef3740d..436c01624746e8bf48a82cb0d0e20cb09b0df633 100644
--- a/composer.json
+++ b/composer.json
@@ -125,15 +125,16 @@
         "drupal/simplesamlphp_auth": "3.0",
         "drupal/smtp": "1.0-beta3",
         "drupal/superfish": "1.2",
-        "drupal/svg_image": "^1.8",
+        "drupal/svg_image": "1.8",
         "drupal/token": "1.0",
         "drupal/userprotect": "1.0",
         "drupal/video_embed_field": "2.0",
         "drupal/views_accordion": "1.0-beta2",
         "drupal/views_autocomplete_filters": "1.1",
         "drupal/views_bootstrap": "3.x-dev",
-        "drupal/views_fieldsets": "^3.3",
-        "drupal/views_infinite_scroll": "^1.5",
+        "drupal/views_bulk_operations": "2.4",
+        "drupal/views_fieldsets": "3.3",
+        "drupal/views_infinite_scroll": "1.5",
         "drupal/views_slideshow": "4.4",
         "drupal/webform": "5.0-rc12",
         "drupal/webform_views": "5.0-alpha2",
diff --git a/composer.lock b/composer.lock
index d65e2754a879292f662d7fb810116dc74cc0dedf..423bd6c964a950eaddd0c2afed834b0bb918a529 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "0ba944d812e267fc1880081644b14083",
+    "content-hash": "844e9f7f4727de9fb76de196d29d0a23",
     "packages": [
         {
             "name": "alchemy/zippy",
@@ -6399,6 +6399,76 @@
                 "source": "http://cgit.drupalcode.org/views_bootstrap"
             }
         },
+        {
+            "name": "drupal/views_bulk_operations",
+            "version": "2.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://git.drupal.org/project/views_bulk_operations",
+                "reference": "8.x-2.4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://ftp.drupal.org/files/projects/views_bulk_operations-8.x-2.4.zip",
+                "reference": "8.x-2.4",
+                "shasum": "50c5778770f3a92e38ecf664301b77146e3cc931"
+            },
+            "require": {
+                "drupal/core": "^8.4"
+            },
+            "type": "drupal-module",
+            "extra": {
+                "branch-alias": {
+                    "dev-2.x": "2.x-dev"
+                },
+                "drupal": {
+                    "version": "8.x-2.4",
+                    "datestamp": "1530516821",
+                    "security-coverage": {
+                        "status": "covered",
+                        "message": "Covered by Drupal's security advisory policy"
+                    }
+                },
+                "drush": {
+                    "services": {
+                        "drush.services.yml": "^9"
+                    }
+                }
+            },
+            "notification-url": "https://packages.drupal.org/8/downloads",
+            "license": [
+                "GPL-2.0+"
+            ],
+            "authors": [
+                {
+                    "name": "Marcin Grabias",
+                    "homepage": "https://www.drupal.org/u/graber"
+                },
+                {
+                    "name": "Jon Pugh",
+                    "homepage": "https://www.drupal.org/user/17028"
+                },
+                {
+                    "name": "bojanz",
+                    "homepage": "https://www.drupal.org/user/86106"
+                },
+                {
+                    "name": "infojunkie",
+                    "homepage": "https://www.drupal.org/user/48424"
+                },
+                {
+                    "name": "joelpittet",
+                    "homepage": "https://www.drupal.org/user/160302"
+                }
+            ],
+            "description": "Adds an ability to perform bulk operations on selected entities from view results. Provides an API to create such operations.",
+            "homepage": "https://www.drupal.org/project/views_bulk_operations",
+            "support": {
+                "source": "http://cgit.drupalcode.org/views_bulk_operations",
+                "issues": "https://www.drupal.org/project/issues/views_bulk_operations?version=8.x",
+                "docs": "https://www.drupal.org/docs/8/modules/views-bulk-operations-vbo"
+            }
+        },
         {
             "name": "drupal/views_fieldsets",
             "version": "3.3.0",
@@ -7636,6 +7706,11 @@
                 "shasum": ""
             },
             "type": "drupal-library",
+            "extra": {
+                "patches_applied": {
+                    "Fontawesome Tags": "patches/superfish-fontawesome-tags.patch"
+                }
+            },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "MIT"
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 9b12a1f699d0b83bc50d396af4f5bc71ad4fd114..b3a74ee4e0a57598b978d3dc9082f494b85bc111 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -6600,6 +6600,78 @@
             "source": "http://cgit.drupalcode.org/views_bootstrap"
         }
     },
+    {
+        "name": "drupal/views_bulk_operations",
+        "version": "2.4.0",
+        "version_normalized": "2.4.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://git.drupal.org/project/views_bulk_operations",
+            "reference": "8.x-2.4"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://ftp.drupal.org/files/projects/views_bulk_operations-8.x-2.4.zip",
+            "reference": "8.x-2.4",
+            "shasum": "50c5778770f3a92e38ecf664301b77146e3cc931"
+        },
+        "require": {
+            "drupal/core": "^8.4"
+        },
+        "type": "drupal-module",
+        "extra": {
+            "branch-alias": {
+                "dev-2.x": "2.x-dev"
+            },
+            "drupal": {
+                "version": "8.x-2.4",
+                "datestamp": "1530516821",
+                "security-coverage": {
+                    "status": "covered",
+                    "message": "Covered by Drupal's security advisory policy"
+                }
+            },
+            "drush": {
+                "services": {
+                    "drush.services.yml": "^9"
+                }
+            }
+        },
+        "installation-source": "dist",
+        "notification-url": "https://packages.drupal.org/8/downloads",
+        "license": [
+            "GPL-2.0+"
+        ],
+        "authors": [
+            {
+                "name": "Marcin Grabias",
+                "homepage": "https://www.drupal.org/u/graber"
+            },
+            {
+                "name": "Jon Pugh",
+                "homepage": "https://www.drupal.org/user/17028"
+            },
+            {
+                "name": "bojanz",
+                "homepage": "https://www.drupal.org/user/86106"
+            },
+            {
+                "name": "infojunkie",
+                "homepage": "https://www.drupal.org/user/48424"
+            },
+            {
+                "name": "joelpittet",
+                "homepage": "https://www.drupal.org/user/160302"
+            }
+        ],
+        "description": "Adds an ability to perform bulk operations on selected entities from view results. Provides an API to create such operations.",
+        "homepage": "https://www.drupal.org/project/views_bulk_operations",
+        "support": {
+            "source": "http://cgit.drupalcode.org/views_bulk_operations",
+            "issues": "https://www.drupal.org/project/issues/views_bulk_operations?version=8.x",
+            "docs": "https://www.drupal.org/docs/8/modules/views-bulk-operations-vbo"
+        }
+    },
     {
         "name": "drupal/views_fieldsets",
         "version": "3.3.0",
diff --git a/web/modules/views_bulk_operations/LICENSE.txt b/web/modules/views_bulk_operations/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1
--- /dev/null
+++ b/web/modules/views_bulk_operations/LICENSE.txt
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/web/modules/views_bulk_operations/README.txt b/web/modules/views_bulk_operations/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..69a18df9f73ec93c764270bf63dc2786d7be275b
--- /dev/null
+++ b/web/modules/views_bulk_operations/README.txt
@@ -0,0 +1,53 @@
+Introduction
+------------
+
+Views Bulk Operations augments Views by allowing actions
+(provided by Drupal core or contrib modules) to be executed
+on the selected view rows.
+
+It does so by showing a checkbox in front of each displayed row, and adding a
+select box on top of the View containing operations that can be applied.
+
+
+Getting started
+-----------------
+
+1. Create a View with a page or block display.
+2. Add a "Views bulk operations" field (global), available on
+   all entity types.
+3. Configure the field by selecting at least one operation.
+4. Go to the View page. VBO functionality should be present.
+
+
+Creating custom actions
+-----------------------
+
+Example that covers different possibilities is available in
+modules/views_bulk_operatios_example/.
+
+In a module, create an action plugin (check the included example module,
+test actions in /tests/views_bulk_operations_test/src/Plugin/Action
+or \core\modules\node\src\Plugin\Action namespace for simple implementations).
+
+Available annotation parameters:
+  - id: The action ID (required),
+  - label: Action label (required),
+  - type: Entity type for the action, if left empty, action will be
+    applicable to all entity types (required),
+  - confirm: If set to TRUE and the next parameter is empty,
+    the module default confirmation form will be used (default: FALSE),
+  - confirm_form_route_name: Route name of the action confirmation form.
+    If left empty and the previous parameter is empty, there will be
+    no confirmation step (default: empty string).
+  - requirements: an array of requirements an action must meet
+    to be displayed on the action selection form. At the moment
+    only one possible requirement is supported: '_permission', if
+    the current user has that permission, the action execution will
+    be possible.
+
+
+Additional notes
+----------------
+
+Full documentation with examples is available at
+https://www.drupal.org/docs/8/modules/views-bulk-operations-vbo.
diff --git a/web/modules/views_bulk_operations/composer.json b/web/modules/views_bulk_operations/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..174bebe31231d1166b7bf0b8eb6b359012ff408f
--- /dev/null
+++ b/web/modules/views_bulk_operations/composer.json
@@ -0,0 +1,26 @@
+{
+  "name": "drupal/views_bulk_operations",
+  "description": "Adds an ability to perform bulk operations on selected entities from view results. Provides an API to create such operations.",
+  "type": "drupal-module",
+  "homepage": "https://www.drupal.org/project/views_bulk_operations",
+  "authors": [
+    {
+      "name": "Marcin Grabias",
+      "homepage": "https://www.drupal.org/u/graber"
+    }
+  ],
+  "support": {
+    "issues": "https://www.drupal.org/project/issues/views_bulk_operations?version=8.x",
+    "docs": "https://www.drupal.org/docs/8/modules/views-bulk-operations-vbo"
+  },
+  "license": "GPL-2.0+",
+  "minimum-stability": "dev",
+  "require": {},
+  "extra": {
+    "drush": {
+      "services": {
+        "drush.services.yml": "^9"
+      }
+    }
+  }
+}
diff --git a/web/modules/views_bulk_operations/config/schema/views_bulk_operations.views.schema.yml b/web/modules/views_bulk_operations/config/schema/views_bulk_operations.views.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..546ae53e556559394491b7f204301b82ccadfe51
--- /dev/null
+++ b/web/modules/views_bulk_operations/config/schema/views_bulk_operations.views.schema.yml
@@ -0,0 +1,25 @@
+views.field.views_bulk_operations_bulk_form:
+  type: views_field
+  label: 'Views Bulk Operations'
+  mapping:
+    batch:
+      type: boolean
+      label: 'Process selected entities in a batch operation'
+    batch_size:
+      type: integer
+      label: 'Size of the processing batch'
+    form_step:
+      type: boolean
+      label: 'Display configuration form on a separate page'
+    buttons:
+      type: boolean
+      label: 'Display action options as buttons'
+    action_title:
+      type: string
+      label: 'Title of the action selector form element'
+    selected_actions:
+      type: ignore
+      label: 'Selected actions array'
+    preconfiguration:
+      type: ignore
+      label: 'Preliminary configuration array'
diff --git a/web/modules/views_bulk_operations/css/frontUi.css b/web/modules/views_bulk_operations/css/frontUi.css
new file mode 100644
index 0000000000000000000000000000000000000000..e5fb3c4578e4fe923206597c547038c3e7967011
--- /dev/null
+++ b/web/modules/views_bulk_operations/css/frontUi.css
@@ -0,0 +1,6 @@
+.views-table-row-vbo-select-all div {
+  text-align: center;
+}
+.views-field-views-bulk-operations-bulk-form.empty {
+  display: none;
+}
diff --git a/web/modules/views_bulk_operations/drush.services.yml b/web/modules/views_bulk_operations/drush.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3d629c0fafacef52f7a20533d59a1a6923ef23c6
--- /dev/null
+++ b/web/modules/views_bulk_operations/drush.services.yml
@@ -0,0 +1,9 @@
+services:
+  views_bulk_operations.commands:
+    class: \Drupal\views_bulk_operations\Commands\ViewsBulkOperationsCommands
+    arguments:
+      - '@current_user'
+      - '@views_bulk_operations.data'
+      - '@plugin.manager.views_bulk_operations_action'
+    tags:
+      - { name: drush.command }
diff --git a/web/modules/views_bulk_operations/js/adminUi.js b/web/modules/views_bulk_operations/js/adminUi.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a2b3f64dc56eb6d17a41be2892ccd94ba8c2355
--- /dev/null
+++ b/web/modules/views_bulk_operations/js/adminUi.js
@@ -0,0 +1,58 @@
+/**
+ * @file
+ * Views admin UI functionality.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.views_bulk_operations = {
+    attach: function (context, settings) {
+      $('.views-bulk-operations-ui').once('views-bulk-operations-ui').each(Drupal.viewsBulkOperationsUi);
+    }
+  };
+
+  /**
+   * Callback used in {@link Drupal.behaviors.views_bulk_operations}.
+   */
+  Drupal.viewsBulkOperationsUi = function () {
+    var uiElement = $(this);
+
+    // Show / hide actions' preliminary configuration.
+    uiElement.find('.vbo-action-state').each(function () {
+      var matches = $(this).attr('name').match(/.*\[.*?\]\[(.*?)\]\[.*?\]/);
+      if (typeof (matches[1]) != 'undefined') {
+        var preconfigurationElement = uiElement.find('*[data-for="' + matches[1] + '"]');
+        $(this).change(function (event) {
+          if ($(this).is(':checked')) {
+            preconfigurationElement.show('fast');
+          }
+          else {
+            preconfigurationElement.hide('fast');
+          }
+        });
+      }
+    });
+
+    // Select / deselect all functionality.
+    var actionsElementWrapper = uiElement.find('details.vbo-actions-widget > .details-wrapper');
+    if (actionsElementWrapper.length) {
+      var checked = false;
+      var allHandle = $('<a href="#" class="vbo-all-switch">' + Drupal.t('Select / deselect all') + '</a>');
+      actionsElementWrapper.prepend(allHandle);
+      allHandle.on('click', function (event) {
+        event.preventDefault();
+        checked = !checked;
+        actionsElementWrapper.find('.vbo-action-state').each(function () {
+          $(this).prop('checked', checked);
+          $(this).trigger('change');
+        });
+        return false;
+      });
+    }
+  };
+})(jQuery, Drupal);
diff --git a/web/modules/views_bulk_operations/js/frontUi.js b/web/modules/views_bulk_operations/js/frontUi.js
new file mode 100644
index 0000000000000000000000000000000000000000..254c8a6557bf041169b6cf403294ceaff43c0a29
--- /dev/null
+++ b/web/modules/views_bulk_operations/js/frontUi.js
@@ -0,0 +1,234 @@
+/**
+ * @file
+ * Select-All Button functionality.
+ */
+
+(function ($, Drupal) {
+
+  'use strict';
+
+  /**
+   * @type {Drupal~behavior}
+   */
+  Drupal.behaviors.views_bulk_operations = {
+    attach: function (context, settings) {
+      $('.vbo-view-form').once('vbo-init').each(Drupal.viewsBulkOperationsFrontUi);
+    }
+  };
+
+  /**
+   * Views Bulk Operation selection object.
+   */
+  Drupal.viewsBulkOperationsSelection = {
+    view_id: '',
+    display_id: '',
+    list: {},
+    $placeholder: null,
+
+    /**
+     * Bind event handlers to an element.
+     *
+     * @param {jQuery} element
+     */
+    bindEventHandlers: function ($element, index) {
+      if ($element.length) {
+        var selectionObject = this;
+        $element.on('keypress', function (event) {
+          // Emulate click action for enter key.
+          if (event.which === 13) {
+            event.preventDefault();
+            event.stopPropagation();
+            selectionObject.update(this.checked, index, $(this).val());
+            $(this).trigger('click');
+          }
+          if (event.which === 32) {
+            selectionObject.update(this.checked, index, $(this).val());
+          }
+        });
+        $element.on('mousedown', function (event) {
+          // Act only on left button click.
+          if (event.which === 1) {
+            selectionObject.update(this.checked, index, $(this).val());
+          }
+        });
+      }
+    },
+
+    /**
+     * Perform an AJAX request to update selection.
+     *
+     * @param {bool} state
+     * @param {string} value
+     */
+    update: function (state, index, value) {
+      if (value === undefined) {
+        value = null;
+      }
+      if (this.view_id.length && this.display_id.length) {
+        var list = {};
+        if (value && value != 'on') {
+          list[value] = this.list[index][value];
+        }
+        else {
+          list = this.list[index];
+        }
+        var op = state ? 'remove' : 'add';
+
+        var $placeholder = this.$placeholder;
+        var target_uri = '/' + drupalSettings.path.pathPrefix + 'views-bulk-operations/ajax/' + this.view_id + '/' + this.display_id;
+        $.ajax(target_uri, {
+          method: 'POST',
+          data: {
+            list: list,
+            op: op
+          },
+          success: function (data) {
+            var count = parseInt($placeholder.text());
+            count += data.change;
+            $placeholder.text(count);
+          }
+        });
+      }
+    }
+  }
+
+  /**
+   * Callback used in {@link Drupal.behaviors.views_bulk_operations}.
+   */
+  Drupal.viewsBulkOperationsFrontUi = function () {
+    var $vboForm = $(this);
+    var $viewsTables = $('.vbo-table', $vboForm);
+    var $primarySelectAll = $('.vbo-select-all', $vboForm);
+    var tableSelectAll = [];
+
+    // When grouping is enabled, there can be multiple tables.
+    if ($viewsTables.length) {
+      $viewsTables.each(function (index) {
+        tableSelectAll[index] = $(this).find('.select-all input').first();
+      });
+      var $tableSelectAll = $(tableSelectAll);
+    }
+
+    // Add AJAX functionality to table checkboxes.
+    var $multiSelectElement = $vboForm.find('.vbo-multipage-selector').first();
+    if ($multiSelectElement.length) {
+
+      Drupal.viewsBulkOperationsSelection.$placeholder = $multiSelectElement.find('.placeholder').first();
+      Drupal.viewsBulkOperationsSelection.view_id = $multiSelectElement.attr('data-view-id');
+      Drupal.viewsBulkOperationsSelection.display_id = $multiSelectElement.attr('data-display-id');
+
+      // Get the list of all checkbox values and add AJAX callback.
+      Drupal.viewsBulkOperationsSelection.list = [];
+
+      var $contentWrappers;
+      if ($viewsTables.length) {
+        $contentWrappers = $viewsTables;
+      }
+      else {
+        $contentWrappers = $([$vboForm]);
+      }
+
+      $contentWrappers.each(function (index) {
+        var $contentWrapper = $(this);
+        Drupal.viewsBulkOperationsSelection.list[index] = {};
+
+        $contentWrapper.find('.views-field-views-bulk-operations-bulk-form input[type="checkbox"]').each(function () {
+          var value = $(this).val();
+          if (value != 'on') {
+            Drupal.viewsBulkOperationsSelection.list[index][value] = $(this).parent().find('label').first().text();
+            Drupal.viewsBulkOperationsSelection.bindEventHandlers($(this), index);
+          }
+        });
+
+        // Bind event handlers to select all checkbox.
+        if ($viewsTables.length && tableSelectAll.length) {
+          Drupal.viewsBulkOperationsSelection.bindEventHandlers(tableSelectAll[index], index);
+        }
+      });
+    }
+
+    // Initialize all selector if the primary select all and
+    // view table elements exist.
+    if ($primarySelectAll.length && $viewsTables.length) {
+      var strings = {
+        selectAll: $('label', $primarySelectAll.parent()).html(),
+        selectRegular: Drupal.t('Select only items on this page')
+      };
+
+      $primarySelectAll.parent().hide();
+
+      if ($viewsTables.length == 1) {
+        var colspan = $('thead th', $viewsTables.first()).length;
+        var $allSelector = $('<tr class="views-table-row-vbo-select-all even" style="display: none"><td colspan="' + colspan + '"><div><input type="submit" class="form-submit" value="' + strings.selectAll + '"></div></td></tr>');
+        $('tbody', $viewsTables.first()).prepend($allSelector);
+      }
+      else {
+        var $allSelector = $('<div class="views-table-row-vbo-select-all" style="display: none"><div><input type="submit" class="form-submit" value="' + strings.selectAll + '"></div></div>');
+        $($viewsTables.first()).before($allSelector);
+      }
+
+      if ($primarySelectAll.is(':checked')) {
+        $('input', $allSelector).val(strings.selectRegular);
+        $allSelector.show();
+      }
+      else {
+        var show_all_selector = true;
+        $tableSelectAll.each(function () {
+          if (!$(this).is(':checked')) {
+            show_all_selector = false;
+          }
+        });
+        if (show_all_selector) {
+          $allSelector.show();
+        }
+      }
+
+      $('input', $allSelector).on('click', function (event) {
+        event.preventDefault();
+        if ($primarySelectAll.is(':checked')) {
+          $multiSelectElement.show('fast');
+          $primarySelectAll.prop('checked', false);
+          $allSelector.removeClass('all-selected');
+          $(this).val(strings.selectAll);
+        }
+        else {
+          $multiSelectElement.hide('fast');
+          $primarySelectAll.prop('checked', true);
+          $allSelector.addClass('all-selected');
+          $(this).val(strings.selectRegular);
+        }
+      });
+
+      $(tableSelectAll).each(function () {
+        $(this).on('change', function (event) {
+          var show_all_selector = true;
+          $tableSelectAll.each(function () {
+            if (!$(this).is(':checked')) {
+              show_all_selector = false;
+            }
+          });
+          if (show_all_selector) {
+            $allSelector.show();
+          }
+          else {
+            $allSelector.hide();
+            if ($primarySelectAll.is(':checked')) {
+              $('input', $allSelector).trigger('click');
+            }
+          }
+        });
+      });
+    }
+    else {
+      $primarySelectAll.first().on('change', function (event) {
+        if (this.checked) {
+          $multiSelectElement.hide('fast');
+        }
+        else {
+          $multiSelectElement.show('fast');
+        }
+      });
+    }
+  };
+
+})(jQuery, Drupal);
diff --git a/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.info.yml b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..562a3de87ac6cb8409d094e1365692f1781749ed
--- /dev/null
+++ b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.info.yml
@@ -0,0 +1,13 @@
+type: module
+name: 'Actions Permissions'
+description: 'Adds access permissions on all actions allowing admins to restrict access on a per-role basis.'
+package: 'Views Bulk Operations'
+# core: 8.x
+dependencies:
+  - drupal:views_bulk_operations
+
+# Information added by Drupal.org packaging script on 2018-07-02
+version: '8.x-2.4'
+core: '8.x'
+project: 'views_bulk_operations'
+datestamp: 1530516826
diff --git a/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.permissions.yml b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e2ccdd1f01915b1a0210014861b8e9bfc01ea57a
--- /dev/null
+++ b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.permissions.yml
@@ -0,0 +1,2 @@
+permission_callbacks:
+  - \Drupal\actions_permissions\ActionsPermissions::permissions
diff --git a/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.services.yml b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a72918162b9f1ec929ff1a6186dd0e6618e27dc2
--- /dev/null
+++ b/web/modules/views_bulk_operations/modules/actions_permissions/actions_permissions.services.yml
@@ -0,0 +1,5 @@
+services:
+  actions_permissions.views_bulk_operations_actions:
+    class: Drupal\actions_permissions\EventSubscriber\ActionsPermissionsEventSubscriber
+    tags:
+      - { name: event_subscriber }
diff --git a/web/modules/views_bulk_operations/modules/actions_permissions/src/ActionsPermissions.php b/web/modules/views_bulk_operations/modules/actions_permissions/src/ActionsPermissions.php
new file mode 100644
index 0000000000000000000000000000000000000000..3e45e1ea72f216d580352cba383fb3cb63fb813d
--- /dev/null
+++ b/web/modules/views_bulk_operations/modules/actions_permissions/src/ActionsPermissions.php
@@ -0,0 +1,106 @@
+<?php
+
+namespace Drupal\actions_permissions;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Create permissions for existing actions.
+ */
+class ActionsPermissions implements ContainerInjectionInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * VBO Action manager service.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager
+   */
+  protected $actionManager;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager $actionManager
+   *   The action manager.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   Entity type manager.
+   */
+  public function __construct(ViewsBulkOperationsActionManager $actionManager, EntityTypeManagerInterface $entityTypeManager) {
+    $this->actionManager = $actionManager;
+    $this->entityTypeManager = $entityTypeManager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.views_bulk_operations_action'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * Get permissions for Actions.
+   *
+   * @return array
+   *   Permissions array.
+   */
+  public function permissions() {
+    $permissions = [];
+    $entity_type_definitions = $this->entityTypeManager->getDefinitions();
+
+    // Get definitions that will not be altered by actions_permissions.
+    foreach ($this->actionManager->getDefinitions([
+      'skip_actions_permissions' => TRUE,
+      'nocache' => TRUE,
+    ]) as $definition) {
+
+      // Skip actions that define their own requirements.
+      if (!empty($definition['requirements'])) {
+        continue;
+      }
+
+      $id = 'execute ' . $definition['id'];
+      $entity_type = NULL;
+      if (empty($definition['type'])) {
+        $entity_type = $this->t('all entity types');
+        $id .= ' all';
+      }
+      elseif (isset($entity_type_definitions[$definition['type']])) {
+        $entity_type = $entity_type_definitions[$definition['type']]->getLabel();
+        $id .= ' ' . $definition['type'];
+      }
+
+      if (isset($entity_type)) {
+        $permissions[$id] = [
+          'title' => $this->t('Execute the %action action on %type.', [
+            '%action' => $definition['label'],
+            '%type' => $entity_type,
+          ]),
+        ];
+      }
+    }
+
+    // Rebuild VBO action definitions cache with
+    // included action_permissions modifications.
+    $this->actionManager->getDefinitions([
+      'nocache' => TRUE,
+    ]);
+
+    return $permissions;
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/modules/actions_permissions/src/EventSubscriber/ActionsPermissionsEventSubscriber.php b/web/modules/views_bulk_operations/modules/actions_permissions/src/EventSubscriber/ActionsPermissionsEventSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..695e9942f90f3bc21d318801e9daee1ab150d7c9
--- /dev/null
+++ b/web/modules/views_bulk_operations/modules/actions_permissions/src/EventSubscriber/ActionsPermissionsEventSubscriber.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\actions_permissions\EventSubscriber;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager;
+
+/**
+ * Defines module event subscriber class.
+ *
+ * Alters actions to make use of permissions created by the module.
+ */
+class ActionsPermissionsEventSubscriber implements EventSubscriberInterface {
+
+  // Subscribe to the VBO event with low priority
+  // to let other modules alter requirements first.
+  const PRIORITY = -999;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[ViewsBulkOperationsActionManager::ALTER_ACTIONS_EVENT][] = ['alterActions', static::PRIORITY];
+    return $events;
+  }
+
+  /**
+   * Alter the actions' definitions.
+   *
+   * @var \Symfony\Component\EventDispatcher\Event $event
+   *   The event to respond to.
+   */
+  public function alterActions(Event $event) {
+
+    // Don't alter definitions if this is invoked by the
+    // own permissions creating method.
+    if (!empty($event->alterParameters['skip_actions_permissions'])) {
+      return;
+    }
+
+    foreach ($event->definitions as $action_id => $definition) {
+
+      // Only process actions that don't define their own requirements.
+      if (empty($definition['requirements'])) {
+        $permission_id = 'execute ' . $definition['id'];
+        if (empty($definition['type'])) {
+          $permission_id .= ' all';
+        }
+        else {
+          $permission_id .= ' ' . $definition['type'];
+        }
+        $definition['requirements']['_permission'] = $permission_id;
+        $event->definitions[$action_id] = $definition;
+      }
+    }
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/modules/views_bulk_operations_example/src/Plugin/Action/ViewsBulkOperationExampleAction.php b/web/modules/views_bulk_operations/modules/views_bulk_operations_example/src/Plugin/Action/ViewsBulkOperationExampleAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..d4262e5f6779088fddf126a3b21e6f6ae34072e4
--- /dev/null
+++ b/web/modules/views_bulk_operations/modules/views_bulk_operations_example/src/Plugin/Action/ViewsBulkOperationExampleAction.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\views_bulk_operations_example\Plugin\Action;
+
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsPreconfigurationInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * An example action covering most of the possible options.
+ *
+ * If type is left empty, action will be selectable for all
+ * entity types.
+ *
+ * @Action(
+ *   id = "views_bulk_operations_example",
+ *   label = @Translation("VBO example action"),
+ *   type = "",
+ *   confirm = TRUE,
+ * )
+ */
+class ViewsBulkOperationExampleAction extends ViewsBulkOperationsActionBase implements ViewsBulkOperationsPreconfigurationInterface, PluginFormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    /*
+     * All config resides in $this->configuration.
+     * Passed view rows will be available in $this->context.
+     * Data about the view used to select results and optionally
+     * the batch context are available in $this->context or externally
+     * through the public getContext() method.
+     * The entire ViewExecutable object  with selected result
+     * rows is available in $this->view or externally through
+     * the public getView() method.
+     */
+
+    // Do some processing..
+    // ...
+    drupal_set_message($entity->label());
+    return sprintf('Example action (configuration: %s)', print_r($this->configuration, TRUE));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildPreConfigurationForm(array $form, array $values, FormStateInterface $form_state) {
+    $form['example_preconfig_setting'] = [
+      '#title' => $this->t('Example setting'),
+      '#type' => 'textfield',
+      '#default_value' => isset($values['example_preconfig_setting']) ? $values['example_preconfig_setting'] : '',
+    ];
+    return $form;
+  }
+
+  /**
+   * Configuration form builder.
+   *
+   * If this method has implementation, the action is
+   * considered to be configurable.
+   *
+   * @param array $form
+   *   Form array.
+   * @param Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
+   *
+   * @return array
+   *   The configuration form.
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['example_config_setting'] = [
+      '#title' => t('Example setting pre-execute'),
+      '#type' => 'textfield',
+      '#default_value' => $form_state->getValue('example_config_setting'),
+    ];
+    return $form;
+  }
+
+  /**
+   * Submit handler for the action configuration form.
+   *
+   * If not implemented, the cleaned form values will be
+   * passed direclty to the action $configuration parameter.
+   *
+   * @param array $form
+   *   Form array.
+   * @param Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    // This is not required here, when this method is not defined,
+    // form values are assigned to the action configuration by default.
+    // This function is a must only when user input processing is needed.
+    $this->configuration['example_config_setting'] = $form_state->getValue('example_config_setting');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    if ($object->getEntityType() === 'node') {
+      $access = $object->access('update', $account, TRUE)
+        ->andIf($object->status->access('edit', $account, TRUE));
+      return $return_as_object ? $access : $access->isAllowed();
+    }
+
+    // Other entity types may have different
+    // access methods and properties.
+    return TRUE;
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/modules/views_bulk_operations_example/views_bulk_operations_example.info.yml b/web/modules/views_bulk_operations/modules/views_bulk_operations_example/views_bulk_operations_example.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4eff3573ec2f956f51267b1bf3edfaefeb43fb72
--- /dev/null
+++ b/web/modules/views_bulk_operations/modules/views_bulk_operations_example/views_bulk_operations_example.info.yml
@@ -0,0 +1,13 @@
+type: module
+name: 'Views Bulk Operations example'
+description: 'Defines an example action with all possible options.'
+package: 'Examples'
+# core: 8.x
+dependencies:
+  - drupal:views_bulk_operations
+
+# Information added by Drupal.org packaging script on 2018-07-02
+version: '8.x-2.4'
+core: '8.x'
+project: 'views_bulk_operations'
+datestamp: 1530516826
diff --git a/web/modules/views_bulk_operations/src/Access/ViewsBulkOperationsAccess.php b/web/modules/views_bulk_operations/src/Access/ViewsBulkOperationsAccess.php
new file mode 100644
index 0000000000000000000000000000000000000000..9f78a983bec6a14be3f84f4a2fac6054e179ef23
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Access/ViewsBulkOperationsAccess.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Access;
+
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Routing\RouteMatch;
+use Drupal\Core\Access\AccessResult;
+use Drupal\views\Views;
+
+/**
+ * Defines module access rules.
+ */
+class ViewsBulkOperationsAccess implements AccessInterface {
+
+  /**
+   * Temporary user storage object.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
+  /**
+   * Object constructor.
+   */
+  public function __construct(PrivateTempStoreFactory $tempStoreFactory) {
+    $this->tempStoreFactory = $tempStoreFactory;
+  }
+
+  /**
+   * A custom access check.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Run access checks for this account.
+   * @param \Drupal\Core\Routing\RouteMatch $routeMatch
+   *   The matched route.
+   */
+  public function access(AccountInterface $account, RouteMatch $routeMatch) {
+    $parameters = $routeMatch->getParameters()->all();
+
+    if ($view = Views::getView($parameters['view_id'])) {
+      if ($view->access($parameters['display_id'], $account)) {
+        return AccessResult::allowed();
+      }
+    }
+    return AccessResult::forbidden();
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionBase.php b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..2209fad10f0a5540bae13d894b106ca46984712b
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionBase.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Action;
+
+use Drupal\Core\Action\ActionBase;
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\views\ViewExecutable;
+
+/**
+ * Views Bulk Operations action plugin base.
+ *
+ * Provides a base implementation for a configurable
+ * and preconfigurable VBO Action plugin.
+ */
+abstract class ViewsBulkOperationsActionBase extends ActionBase implements ViewsBulkOperationsActionInterface, ConfigurablePluginInterface {
+
+  /**
+   * Action context.
+   *
+   * @var array
+   *   Contains view data and optionally batch operation context.
+   */
+  protected $context;
+
+  /**
+   * The processed view.
+   *
+   * @var \Drupal\views\ViewExecutable
+   */
+  protected $view;
+
+  /**
+   * Configuration array.
+   *
+   * @var array
+   */
+  protected $configuration;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setContext(array &$context) {
+    $this->context['sandbox'] = &$context['sandbox'];
+    foreach ($context as $key => $item) {
+      if ($key === 'sandbox') {
+        continue;
+      }
+      $this->context[$key] = $item;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setView(ViewExecutable $view) {
+    $this->view = $view;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeMultiple(array $objects) {
+    $results = [];
+    foreach ($objects as $entity) {
+      $results[] = $this->execute($entity);
+    }
+
+    return $results;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [];
+  }
+
+  /**
+   * Default configuration form validator.
+   *
+   * This method will be needed if a child class will implement
+   * \Drupal\Core\Plugin\PluginFormInterface. Code saver.
+   *
+   * @param array &$form
+   *   Form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+
+  }
+
+  /**
+   * Default configuration form submit handler.
+   *
+   * This method will be needed if a child class will implement
+   * \Drupal\Core\Plugin\PluginFormInterface. Code saver.
+   *
+   * @param array &$form
+   *   Form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $form_state->cleanValues();
+    foreach ($form_state->getValues() as $key => $value) {
+      $this->configuration[$key] = $value;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = $configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return [];
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionInterface.php b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..d6c8824a4ed1fce71e01ccccbe9e3311fe4e7a80
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsActionInterface.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Action;
+
+use Drupal\views\ViewExecutable;
+
+/**
+ * Defines Views Bulk Operations action interface.
+ */
+interface ViewsBulkOperationsActionInterface {
+
+  /**
+   * Set action context.
+   *
+   * Implementation should have an option to add data to the
+   * context, not overwrite it on every method execution.
+   *
+   * @param array $context
+   *   The context array.
+   *
+   * @see ViewsBulkOperationsActionBase::setContext
+   */
+  public function setContext(array &$context);
+
+  /**
+   * Set view object.
+   *
+   * @param \Drupal\views\ViewExecutable $view
+   *   The processed view.
+   */
+  public function setView(ViewExecutable $view);
+
+  /**
+   * Execute action on multiple entities.
+   *
+   * Can return an array of results of processing, if no return value
+   * is provided, action label will be used for each result.
+   *
+   * @param array $objects
+   *   An array of entities.
+   *
+   * @return array
+   *   An array of translatable markup objects or strings (optional)
+   */
+  public function executeMultiple(array $objects);
+
+}
diff --git a/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsPreconfigurationInterface.php b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsPreconfigurationInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..2f87d43b9ad28e3e3ccb613a4eb90b66e9428f80
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Action/ViewsBulkOperationsPreconfigurationInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Action;
+
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines methods for a preconfigurable Views Bulk Operations action.
+ */
+interface ViewsBulkOperationsPreconfigurationInterface {
+
+  /**
+   * Build preconfigure action form elements.
+   *
+   * @param array $element
+   *   Element of the views API form where configuration resides.
+   * @param array $values
+   *   Current values of the plugin pre-configuration.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   Form state interface object.
+   *
+   * @return array
+   *   The action configuration form element.
+   */
+  public function buildPreConfigurationForm(array $element, array $values, FormStateInterface $form_state);
+
+}
diff --git a/web/modules/views_bulk_operations/src/Commands/ViewsBulkOperationsCommands.php b/web/modules/views_bulk_operations/src/Commands/ViewsBulkOperationsCommands.php
new file mode 100644
index 0000000000000000000000000000000000000000..9c9d2f14bf547c571f4bcc6db14695b296e22aa8
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Commands/ViewsBulkOperationsCommands.php
@@ -0,0 +1,287 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Commands;
+
+use Drush\Commands\DrushCommands;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\views_bulk_operations\Service\ViewsbulkOperationsViewDataInterface;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager;
+use Drupal\user\Entity\User;
+use Drupal\views\Views;
+use Drupal\views_bulk_operations\ViewsBulkOperationsBatch;
+
+/**
+ * Defines Drush commands for the module.
+ */
+class ViewsBulkOperationsCommands extends DrushCommands {
+
+  /**
+   * The current user object.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Object that gets the current view data.
+   *
+   * @var \Drupal\views_bulk_operations\ViewsbulkOperationsViewDataInterface
+   */
+  protected $viewData;
+
+  /**
+   * Views Bulk Operations action manager.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager
+   */
+  protected $actionManager;
+
+  /**
+   * ViewsBulkOperationsCommands object constructor.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user object.
+   * @param \Drupal\views_bulk_operations\ViewsbulkOperationsViewDataInterface $viewData
+   *   VBO View data service.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager $actionManager
+   *   VBO Action manager service.
+   */
+  public function __construct(
+    AccountInterface $currentUser,
+    ViewsbulkOperationsViewDataInterface $viewData,
+    ViewsBulkOperationsActionManager $actionManager
+  ) {
+    $this->currentUser = $currentUser;
+    $this->viewData = $viewData;
+    $this->actionManager = $actionManager;
+  }
+
+  /**
+   * Execute an action on all results of the specified view.
+   *
+   * Use the --verbose parameter to see progress messages.
+   *
+   * @param string $view_id
+   *   The ID of the view to use.
+   * @param string $action_id
+   *   The ID of the action to execute.
+   * @param array $options
+   *   (optional) An array of options.
+   *
+   * @return string
+   *   The summary message.
+   *
+   * @command views-bulk-operations:execute
+   *
+   * @option display-id
+   *   ID of the display to use.
+   * @option args
+   *   View arguments (slash is a delimeter).
+   * @option exposed
+   *   Exposed filters (query string format).
+   * @option batch-size
+   *   Processing batch size.
+   * @option configuration
+   *   Action configuration (query string format).
+   * @option user-id
+   *   The ID of the user account used for performing the operation.
+   *
+   * @usage drush views-bulk-operations:execute some_view some_action
+   *   Execute some action on some view.
+   * @usage drush vbo-execute some_view some_action --args=arg1/arg2 --batch-size=50
+   *   Execute some action on some view with arg1 and arg2 as
+   *   the view arguments and 50 entities processed per batch.
+   * @usage drush vbo-exec some_view some_action --configuration=&quot;key1=value1&amp;key2=value2&quot;
+   *   Execute some action on some view with the specified action configuration.
+   *
+   * @aliases vbo-execute, vbo-exec
+   */
+  public function vboExecute(
+    $view_id,
+    $action_id,
+    array $options = [
+      'display-id' => 'default',
+      'args' => '',
+      'exposed' => '',
+      'batch-size' => 100,
+      'configuration' => '',
+      'user-id' => 1,
+    ]
+  ) {
+
+    if (empty($view_id) || empty($action_id)) {
+      throw new \Exception($this->t('You must specify the view ID and the action ID parameters.'));
+    }
+
+    $this->timer($options['verbose']);
+
+    // Prepare options.
+    if ($options['args']) {
+      $options['args'] = explode('/', $options['args']);
+    }
+    else {
+      $options['args'] = [];
+    }
+
+    // Decode query string format options.
+    foreach (['configuration', 'exposed'] as $name) {
+      if (!empty($options[$name]) && !is_array($options[$name])) {
+        parse_str($options[$name], $options[$name]);
+      }
+      else {
+        $options[$name] = [];
+      }
+    }
+
+    $vbo_data = [
+      'list' => [],
+      'view_id' => $view_id,
+      'display_id' => $options['display-id'],
+      'action_id' => $action_id,
+      'preconfiguration' => $options['configuration'],
+      'batch' => TRUE,
+      'arguments' => $options['args'],
+      'exposed_input' => $options['exposed'],
+      'batch_size' => $options['batch-size'],
+      'relationship_id' => 'none',
+    ];
+
+    // Login as superuser, as drush 9 doesn't support the
+    // --user parameter.
+    $account = User::load($options['user-id']);
+    $this->currentUser->setAccount($account);
+
+    // Initialize the view to check if parameters are correct.
+    if (!$view = Views::getView($vbo_data['view_id'])) {
+      throw new \Exception($this->t('Incorrect view ID provided.'));
+    }
+    if (!$view->setDisplay($vbo_data['display_id'])) {
+      throw new \Exception($this->t('Incorrect view display ID provided.'));
+    }
+    if (!empty($vbo_data['arguments'])) {
+      $view->setArguments($vbo_data['arguments']);
+    }
+    if (!empty($vbo_data['exposed_input'])) {
+      $view->setExposedInput($vbo_data['exposed_input']);
+    }
+
+    // We need total rows count for proper progress message display.
+    $view->get_total_rows = TRUE;
+    $view->execute();
+
+    // Get relationship ID if VBO field exists.
+    $vbo_data['relationship_id'] = 'none';
+    foreach ($view->field as $field) {
+      if ($field->options['id'] === 'views_bulk_operations_bulk_form') {
+        $vbo_data['relationship_id'] = $field->options['relationship'];
+      }
+    }
+
+    // Get total rows count.
+    $this->viewData->init($view, $view->getDisplay(), $vbo_data['relationship_id']);
+    $vbo_data['total_results'] = $this->viewData->getTotalResults();
+
+    // Get action definition and check if action ID is correct.
+    $action_definition = $this->actionManager->getDefinition($action_id);
+    $vbo_data['action_label'] = (string) $action_definition['label'];
+
+    $this->timer($options['verbose'], 'init');
+
+    // Populate entity list.
+    $context = [];
+    do {
+      $context['finished'] = 1;
+      $context['message'] = '';
+      ViewsBulkOperationsBatch::getList($vbo_data, $context);
+      if (!empty($context['message'])) {
+        $this->logger->info($context['message']);
+      }
+    } while ($context['finished'] < 1);
+    $vbo_data = $context['results'];
+
+    $this->timer($options['verbose'], 'list');
+
+    // Execute the selected action.
+    $context = [];
+    do {
+      $context['finished'] = 1;
+      $context['message'] = '';
+      ViewsBulkOperationsBatch::operation($vbo_data, $context);
+      if (!empty($context['message'])) {
+        $this->logger->info($context['message']);
+      }
+    } while ($context['finished'] < 1);
+
+    // Output a summary message.
+    $operations = array_count_values($context['results']['operations']);
+    $details = [];
+    foreach ($operations as $op => $count) {
+      $details[] = $op . ' (' . $count . ')';
+    }
+
+    // Display debug information.
+    if ($options['verbose']) {
+      $this->timer($options['verbose'], 'execute');
+      $this->logger->info($this->t('Initialization time: @time ms.', ['@time' => $this->timer($options['verbose'], 'init')]));
+      $this->logger->info($this->t('Entity list generation time: @time ms.', ['@time' => $this->timer($options['verbose'], 'list')]));
+      $this->logger->info($this->t('Execution time: @time ms.', ['@time' => $this->timer($options['verbose'], 'execute')]));
+    }
+
+    return $this->t('Action processing results: @results.', ['@results' => implode(', ', $details)]);
+  }
+
+  /**
+   * Helper function to set / get timer.
+   *
+   * @param bool $debug
+   *   Should the function do anything at all?
+   * @param string $id
+   *   ID of a specific timer span.
+   *
+   * @return mixed
+   *   NULL or value of a specific timer if set.
+   */
+  protected function timer($debug = TRUE, $id = NULL) {
+    if (!$debug) {
+      return;
+    }
+
+    static $timers = [];
+
+    if (!isset($id)) {
+      $timers['start'] = microtime(TRUE);
+    }
+    else {
+      if (isset($timers[$id])) {
+        end($timers);
+        do {
+          if (key($timers) === $id) {
+            return round((current($timers) - prev($timers)) * 1000, 3);
+          }
+          else {
+            $result = prev($timers);
+          }
+        } while ($result);
+      }
+      else {
+        $timers[$id] = microtime(TRUE);
+      }
+    }
+  }
+
+  /**
+   * Translates a string using the dt function.
+   *
+   * @param string $message
+   *   The message to translate.
+   * @param array $arguments
+   *   (optional) The translation arguments.
+   *
+   * @return string
+   *   The translated message.
+   */
+  protected function t($message, array $arguments = []) {
+    return dt($message, $arguments);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Controller/ViewsBulkOperationsController.php b/web/modules/views_bulk_operations/src/Controller/ViewsBulkOperationsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..3908916ddac514770d7282e19dcd5d31b6a44af3
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Controller/ViewsBulkOperationsController.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\views_bulk_operations\Form\ViewsBulkOperationsFormTrait;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Drupal\Core\Ajax\AjaxResponse;
+
+/**
+ * Defines VBO controller class.
+ */
+class ViewsBulkOperationsController extends ControllerBase implements ContainerInjectionInterface {
+
+  use ViewsBulkOperationsFormTrait;
+
+  /**
+   * User private temporary storage factory.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
+  /**
+   * Views Bulk Operations action processor.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface
+   */
+  protected $actionProcessor;
+
+  /**
+   * Constructs a new controller object.
+   *
+   * @param \Drupal\user\PrivateTempStoreFactory $tempStoreFactory
+   *   User private temporary storage factory.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface $actionProcessor
+   *   Views Bulk Operations action processor.
+   */
+  public function __construct(
+    PrivateTempStoreFactory $tempStoreFactory,
+    ViewsBulkOperationsActionProcessorInterface $actionProcessor
+  ) {
+    $this->tempStoreFactory = $tempStoreFactory;
+    $this->actionProcessor = $actionProcessor;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.private_tempstore'),
+      $container->get('views_bulk_operations.processor')
+    );
+  }
+
+  /**
+   * The actual page callback.
+   *
+   * @param string $view_id
+   *   The current view ID.
+   * @param string $display_id
+   *   The display ID of the current view.
+   */
+  public function execute($view_id, $display_id) {
+    $view_data = $this->getTempstoreData($view_id, $display_id);
+    if (empty($view_data)) {
+      throw new NotFoundHttpException();
+    }
+    $this->deleteTempstoreData();
+
+    $this->actionProcessor->executeProcessing($view_data);
+    return batch_process($view_data['redirect_url']);
+  }
+
+  /**
+   * AJAX callback to update selection (multipage).
+   *
+   * @param string $view_id
+   *   The current view ID.
+   * @param string $display_id
+   *   The display ID of the current view.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   */
+  public function updateSelection($view_id, $display_id, Request $request) {
+    $view_data = $this->getTempstoreData($view_id, $display_id);
+    if (empty($view_data)) {
+      throw new NotFoundHttpException();
+    }
+
+    $list = $request->request->get('list');
+
+    $op = $request->request->get('op', 'add');
+    $change = 0;
+
+    if ($op === 'add') {
+      foreach ($list as $bulkFormKey => $label) {
+        if (!isset($view_data['list'][$bulkFormKey])) {
+          $view_data['list'][$bulkFormKey] = $this->getListItem($bulkFormKey, $label);
+          $change++;
+        }
+      }
+    }
+    elseif ($op === 'remove') {
+      foreach ($list as $bulkFormKey => $label) {
+        if (isset($view_data['list'][$bulkFormKey])) {
+          unset($view_data['list'][$bulkFormKey]);
+          $change--;
+        }
+      }
+    }
+    $this->setTempstoreData($view_data);
+
+    $response = new AjaxResponse();
+    $response->setData(['change' => $change]);
+    return $response;
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/EventSubscriber/ViewsBulkOperationsEventSubscriber.php b/web/modules/views_bulk_operations/src/EventSubscriber/ViewsBulkOperationsEventSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..6ff28922541f9752997e8599fe74b5542bb73310
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/EventSubscriber/ViewsBulkOperationsEventSubscriber.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\views_bulk_operations\EventSubscriber;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsViewDataInterface;
+use Drupal\views_bulk_operations\ViewsBulkOperationsEvent;
+
+/**
+ * Defines module event subscriber class.
+ *
+ * Allows getting data of core entity views.
+ */
+class ViewsBulkOperationsEventSubscriber implements EventSubscriberInterface {
+
+  // Subscribe to the VBO event with high priority
+  // to prepopulate the event data.
+  const PRIORITY = 999;
+
+  /**
+   * Object that gets the current view data.
+   *
+   * @var \Drupal\views_bulk_operations\ViewsBulkOperationsViewDataInterface
+   */
+  protected $viewData;
+
+  /**
+   * Object constructor.
+   *
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsViewDataInterface $viewData
+   *   The VBO View Data provider service.
+   */
+  public function __construct(ViewsBulkOperationsViewDataInterface $viewData) {
+    $this->viewData = $viewData;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[ViewsBulkOperationsEvent::NAME][] = ['provideViewData', self::PRIORITY];
+    return $events;
+  }
+
+  /**
+   * Respond to view data request event.
+   *
+   * @var \Drupal\views_bulk_operations\ViewsBulkOperationsEvent $event
+   *   The event to respond to.
+   */
+  public function provideViewData(ViewsBulkOperationsEvent $event) {
+    $view_data = $event->getViewData();
+    if (!empty($view_data['table']['entity type'])) {
+      $event->setEntityTypeIds([$view_data['table']['entity type']]);
+      $event->setEntityGetter([
+        'callable' => [$this->viewData, 'getEntityDefault'],
+      ]);
+    }
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Form/ConfigureAction.php b/web/modules/views_bulk_operations/src/Form/ConfigureAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..ba769346acd742bd637fc66b71b8058c97e467bc
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Form/ConfigureAction.php
@@ -0,0 +1,176 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Form;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface;
+
+/**
+ * Action configuration form.
+ */
+class ConfigureAction extends FormBase {
+
+  use ViewsBulkOperationsFormTrait;
+
+  /**
+   * User private temporary storage factory.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
+  /**
+   * Views Bulk Operations action manager.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager
+   */
+  protected $actionManager;
+
+  /**
+   * Views Bulk Operations action processor.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface
+   */
+  protected $actionProcessor;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\user\PrivateTempStoreFactory $tempStoreFactory
+   *   User private temporary storage factory.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager $actionManager
+   *   Extended action manager object.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface $actionProcessor
+   *   Views Bulk Operations action processor.
+   */
+  public function __construct(
+    PrivateTempStoreFactory $tempStoreFactory,
+    ViewsBulkOperationsActionManager $actionManager,
+    ViewsBulkOperationsActionProcessorInterface $actionProcessor
+  ) {
+    $this->tempStoreFactory = $tempStoreFactory;
+    $this->actionManager = $actionManager;
+    $this->actionProcessor = $actionProcessor;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.private_tempstore'),
+      $container->get('plugin.manager.views_bulk_operations_action'),
+      $container->get('views_bulk_operations.processor')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'views_bulk_operations_configure_action';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $view_id = NULL, $display_id = NULL) {
+
+    $form_data = $this->getFormData($view_id, $display_id);
+
+    // TODO: display an error msg, redirect back.
+    if (!isset($form_data['action_id'])) {
+      return;
+    }
+
+    $form['#title'] = $this->t('Configure "%action" action applied to the selection', ['%action' => $form_data['action_label']]);
+
+    $selection = [];
+    if (!empty($form_data['entity_labels'])) {
+      $form['list'] = [
+        '#theme' => 'item_list',
+        '#items' => $form_data['entity_labels'],
+      ];
+    }
+    else {
+      $form['list'] = [
+        '#type' => 'item',
+        '#markup' => $this->t('All view results'),
+      ];
+    }
+    $form['list']['#title'] = $this->t('Selected @count entities:', ['@count' => $form_data['selected_count']]);
+
+    // :D Make sure the submit button is at the bottom of the form
+    // and is editale from the action buildConfigurationForm method.
+    $form['actions']['#weight'] = 666;
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Apply'),
+      '#submit' => [
+        [$this, 'submitForm'],
+      ],
+    ];
+    $this->addCancelButton($form);
+
+    $action = $this->actionManager->createInstance($form_data['action_id']);
+
+    if (method_exists($action, 'setContext')) {
+      $action->setContext($form_data);
+    }
+
+    $form_state->set('views_bulk_operations', $form_data);
+    $form = $action->buildConfigurationForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $form_data = $form_state->get('views_bulk_operations');
+
+    $action = $this->actionManager->createInstance($form_data['action_id']);
+    if (method_exists($action, 'validateConfigurationForm')) {
+      $action->validateConfigurationForm($form, $form_state);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $form_data = $form_state->get('views_bulk_operations');
+
+    $action = $this->actionManager->createInstance($form_data['action_id']);
+    if (method_exists($action, 'submitConfigurationForm')) {
+      $action->submitConfigurationForm($form, $form_state);
+      $form_data['configuration'] = $action->getConfiguration();
+    }
+    else {
+      $form_state->cleanValues();
+      $form_data['configuration'] = $form_state->getValues();
+    }
+
+    $definition = $this->actionManager->getDefinition($form_data['action_id']);
+    if (!empty($definition['confirm_form_route_name'])) {
+      // Update tempStore data.
+      $this->setTempstoreData($form_data, $form_data['view_id'], $form_data['display_id']);
+      // Go to the confirm route.
+      $form_state->setRedirect($definition['confirm_form_route_name'], [
+        'view_id' => $form_data['view_id'],
+        'display_id' => $form_data['display_id'],
+      ]);
+    }
+    else {
+      $this->deleteTempstoreData($form_data['view_id'], $form_data['display_id']);
+      $this->actionProcessor->executeProcessing($form_data);
+      $form_state->setRedirectUrl($form_data['redirect_url']);
+    }
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Form/ConfirmAction.php b/web/modules/views_bulk_operations/src/Form/ConfirmAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..dda696db2a8014d9c3618ce2647ef23839e14dc2
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Form/ConfirmAction.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Form;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface;
+
+/**
+ * Default action execution confirmation form.
+ */
+class ConfirmAction extends FormBase {
+
+  use ViewsBulkOperationsFormTrait;
+
+  /**
+   * User private temporary storage factory.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
+  /**
+   * Views Bulk Operations action manager.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager
+   */
+  protected $actionManager;
+
+  /**
+   * Views Bulk Operations action processor.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface
+   */
+  protected $actionProcessor;
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\user\PrivateTempStoreFactory $tempStoreFactory
+   *   User private temporary storage factory.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager $actionManager
+   *   Extended action manager object.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface $actionProcessor
+   *   Views Bulk Operations action processor.
+   */
+  public function __construct(
+    PrivateTempStoreFactory $tempStoreFactory,
+    ViewsBulkOperationsActionManager $actionManager,
+    ViewsBulkOperationsActionProcessorInterface $actionProcessor
+  ) {
+    $this->tempStoreFactory = $tempStoreFactory;
+    $this->actionManager = $actionManager;
+    $this->actionProcessor = $actionProcessor;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('user.private_tempstore'),
+      $container->get('plugin.manager.views_bulk_operations_action'),
+      $container->get('views_bulk_operations.processor')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'views_bulk_operations_confirm_action';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $view_id = NULL, $display_id = NULL) {
+
+    $form_data = $this->getFormData($view_id, $display_id);
+
+    // TODO: display an error msg, redirect back.
+    if (!isset($form_data['action_id'])) {
+      return;
+    }
+
+    if (!empty($form_data['entity_labels'])) {
+      $form['list'] = [
+        '#theme' => 'item_list',
+        '#items' => $form_data['entity_labels'],
+      ];
+    }
+
+    $form['#title'] = $this->formatPlural(
+      $form_data['selected_count'],
+      'Are you sure you wish to perform "%action" action on 1 entity?',
+      'Are you sure you wish to perform "%action" action on %count entities?',
+      [
+        '%action' => $form_data['action_label'],
+        '%count' => $form_data['selected_count'],
+      ]
+    );
+
+    $form['actions']['submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Execute action'),
+      '#submit' => [
+        [$this, 'submitForm'],
+      ],
+    ];
+    $this->addCancelButton($form);
+
+    $form_state->set('views_bulk_operations', $form_data);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $form_data = $form_state->get('views_bulk_operations');
+    $this->deleteTempstoreData($form_data['view_id'], $form_data['display_id']);
+    $this->actionProcessor->executeProcessing($form_data);
+    $form_state->setRedirectUrl($form_data['redirect_url']);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Form/ViewsBulkOperationsFormTrait.php b/web/modules/views_bulk_operations/src/Form/ViewsBulkOperationsFormTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..fba98fc6070d4dc5cfb3d0263427932bfabbf278
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Form/ViewsBulkOperationsFormTrait.php
@@ -0,0 +1,197 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Form;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Defines common methods for Views Bulk Operations forms.
+ */
+trait ViewsBulkOperationsFormTrait {
+
+  /**
+   * The tempstore object associated with the current view.
+   *
+   * @var \Drupal\user\PrivateTempStore
+   */
+  protected $viewTempstore;
+
+  /**
+   * The tempstore name.
+   *
+   * @var string
+   */
+  protected $tempStoreName;
+
+  /**
+   * Helper function to prepare data needed for proper form display.
+   *
+   * @param string $view_id
+   *   The current view ID.
+   * @param string $display_id
+   *   The current view display ID.
+   *
+   * @return array
+   *   Array containing data for the form builder.
+   */
+  protected function getFormData($view_id, $display_id) {
+
+    // Get tempstore data.
+    $form_data = $this->getTempstoreData($view_id, $display_id);
+
+    // Get data needed for selected entities list.
+    if (!empty($form_data['list'])) {
+      $form_data['entity_labels'] = [];
+      $form_data['selected_count'] = 0;
+      foreach ($form_data['list'] as $item) {
+        $form_data['selected_count']++;
+        $form_data['entity_labels'][] = $item[4];
+      }
+    }
+    elseif ($form_data['total_results']) {
+      $form_data['selected_count'] = $form_data['total_results'];
+    }
+    else {
+      $form_data['selected_count'] = (string) $this->t('all');
+    }
+
+    return $form_data;
+  }
+
+  /**
+   * Calculates the bulk form key for an entity.
+   *
+   * This generates a key that is used as the checkbox return value when
+   * submitting the bulk form.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity to calculate a bulk form key for.
+   * @param mixed $base_field_value
+   *   The value of the base field for this view result.
+   *
+   * @return string
+   *   The bulk form key representing the entity id, language and revision (if
+   *   applicable) as one string.
+   *
+   * @see self::loadEntityFromBulkFormKey()
+   */
+  public static function calculateEntityBulkFormKey(EntityInterface $entity, $base_field_value) {
+    // We don't really need the entity ID or type ID, since only the
+    // base field value and language are used to select rows, but
+    // other modules may need those values.
+    $key_parts = [
+      $base_field_value,
+      $entity->language()->getId(),
+      $entity->getEntityTypeId(),
+      $entity->id(),
+    ];
+
+    // An entity ID could be an arbitrary string (although they are typically
+    // numeric). JSON then Base64 encoding ensures the bulk_form_key is
+    // safe to use in HTML, and that the key parts can be retrieved.
+    $key = json_encode($key_parts);
+    return base64_encode($key);
+  }
+
+  /**
+   * Get an entity list item from a bulk form key and label.
+   *
+   * @param string $bulkFormKey
+   *   A bulk form key.
+   * @param mixed $label
+   *   Entity label, string or
+   *   \Drupal\Core\StringTranslation\TranslatableMarkup.
+   *
+   * @return array
+   *   Entity list item.
+   */
+  protected function getListItem($bulkFormKey, $label) {
+    $item = json_decode(base64_decode($bulkFormKey));
+    $item[] = $label;
+    return $item;
+  }
+
+  /**
+   * Initialize the current view tempstore object.
+   */
+  protected function getTempstore($view_id = NULL, $display_id = NULL) {
+    if (!isset($this->viewTempstore)) {
+      $this->tempStoreName = 'views_bulk_operations_' . $view_id . '_' . $display_id;
+      $this->viewTempstore = $this->tempStoreFactory->get($this->tempStoreName);
+    }
+    return $this->viewTempstore;
+  }
+
+  /**
+   * Gets the current view user tempstore data.
+   *
+   * @param string $view_id
+   *   The current view ID.
+   * @param string $display_id
+   *   The display ID of the current view.
+   */
+  protected function getTempstoreData($view_id = NULL, $display_id = NULL) {
+    $data = $this->getTempstore($view_id, $display_id)->get($this->currentUser()->id());
+
+    return $data;
+  }
+
+  /**
+   * Sets the current view user tempstore data.
+   *
+   * @param array $data
+   *   The data to set.
+   * @param string $view_id
+   *   The current view ID.
+   * @param string $display_id
+   *   The display ID of the current view.
+   */
+  protected function setTempstoreData(array $data, $view_id = NULL, $display_id = NULL) {
+    return $this->getTempstore($view_id, $display_id)->set($this->currentUser()->id(), $data);
+  }
+
+  /**
+   * Deletes the current view user tempstore data.
+   *
+   * @param string $view_id
+   *   The current view ID.
+   * @param string $display_id
+   *   The display ID of the current view.
+   */
+  protected function deleteTempstoreData($view_id = NULL, $display_id = NULL) {
+    return $this->getTempstore($view_id, $display_id)->delete($this->currentUser()->id());
+  }
+
+  /**
+   * Add a cancel button into a VBO form.
+   *
+   * @param array $form
+   *   The form definition.
+   */
+  protected function addCancelButton(array &$form) {
+    $form['actions']['cancel'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Cancel'),
+      '#submit' => [
+        [$this, 'cancelForm'],
+      ],
+      '#limit_validation_errors' => [],
+    ];
+  }
+
+  /**
+   * Submit callback to cancel an action and return to the view.
+   *
+   * @param array $form
+   *   The form definition.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public function cancelForm(array &$form, FormStateInterface $form_state) {
+    $form_data = $form_state->get('views_bulk_operations');
+    drupal_set_message($this->t('Canceled "%action".', ['%action' => $form_data['action_label']]));
+    $form_state->setRedirectUrl($form_data['redirect_url']);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Plugin/Action/CancelUserAction.php b/web/modules/views_bulk_operations/src/Plugin/Action/CancelUserAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..1e73ffc30bb6bac28c97efc109ec802f341e039d
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Plugin/Action/CancelUserAction.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Plugin\Action;
+
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+/**
+ * Cancels a user account.
+ *
+ * @Action(
+ *   id = "vbo_cancel_user_action",
+ *   label = @Translation("Cancel the selected user accounts"),
+ *   type = "user",
+ * )
+ */
+class CancelUserAction extends ViewsBulkOperationsActionBase implements ContainerFactoryPluginInterface, PluginFormInterface {
+
+  /**
+   * The current user.
+   *
+   * @var Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * User module config.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $userConfig;
+
+  /**
+   * Module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Object constructor.
+   *
+   * @param array $configuration
+   *   Plugin configuration.
+   * @param string $plugin_id
+   *   The plugin Id.
+   * @param mixed $plugin_definition
+   *   Plugin definition.
+   * @param Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory object.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   Module handler service.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    AccountInterface $currentUser,
+    ConfigFactoryInterface $configFactory,
+    ModuleHandlerInterface $moduleHandler
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->currentUser = $currentUser;
+    $this->userConfig = $configFactory->get('user.settings');
+    $this->moduleHandler = $moduleHandler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('current_user'),
+      $container->get('config.factory'),
+      $container->get('module_handler')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($account = NULL) {
+    if ($account->id() === $this->currentUser->id() && (empty($this->context['list']) || count($this->context['list'] > 1))) {
+      drupal_set_message($this->t('The current user account cannot be canceled in a batch operation. Select your account only or cancel it from your account page.'), 'error');
+    }
+    elseif (intval($account->id()) === 1) {
+      drupal_set_message($this->t('The user 1 account (%label) cannot be canceled.', [
+        '%label' => $account->label(),
+      ]), 'error');
+    }
+    else {
+      // Allow other modules to act.
+      if ($this->configuration['user_cancel_method'] != 'user_cancel_delete') {
+        $this->moduleHandler->invokeAll('user_cancel', [
+          $this->configuration,
+          $account,
+          $this->configuration['user_cancel_method'],
+        ]);
+      }
+
+      // Cancel the account.
+      _user_cancel($this->configuration, $account, $this->configuration['user_cancel_method']);
+
+      // If current user was cancelled, logout.
+      if ($account->id() == $this->currentUser->id()) {
+        _user_cancel_session_regenerate();
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['user_cancel_method'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('When cancelling these accounts'),
+    ];
+
+    $form['user_cancel_method'] += user_cancel_methods();
+
+    // Allow to send the account cancellation confirmation mail.
+    $form['user_cancel_confirm'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Require email confirmation to cancel account'),
+      '#default_value' => FALSE,
+      '#description' => $this->t('When enabled, the user must confirm the account cancellation via email.'),
+    ];
+    // Also allow to send account canceled notification mail, if enabled.
+    $form['user_cancel_notify'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Notify user when account is canceled'),
+      '#default_value' => FALSE,
+      '#access' => $this->userConfig->get('notify.status_canceled'),
+      '#description' => $this->t('When enabled, the user will receive an email notification after the account has been canceled.'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    /** @var \Drupal\user\UserInterface $object */
+    return $object->access('delete', $account, $return_as_object);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Plugin/Action/EntityDeleteAction.php b/web/modules/views_bulk_operations/src/Plugin/Action/EntityDeleteAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..9243b259c3419df15e67a649d1e4626545d8fa8a
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Plugin/Action/EntityDeleteAction.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Plugin\Action;
+
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Delete entity action with default confirmation form.
+ *
+ * @Action(
+ *   id = "views_bulk_operations_delete_entity",
+ *   label = @Translation("Delete selected entities"),
+ *   type = "",
+ *   confirm = TRUE,
+ * )
+ */
+class EntityDeleteAction extends ViewsBulkOperationsActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    $entity->delete();
+    return $this->t('Delete entities');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    $access = $object->access('delete', $account, TRUE);
+    if ($object->getEntityType() === 'node') {
+      $access->andIf($object->status->access('delete', $account, TRUE));
+    }
+    return $return_as_object ? $access : $access->isAllowed();
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Plugin/views/field/ViewsBulkOperationsBulkForm.php b/web/modules/views_bulk_operations/src/Plugin/views/field/ViewsBulkOperationsBulkForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..66d893f3c6ee7b28746171f9886fdc16501167b5
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Plugin/views/field/ViewsBulkOperationsBulkForm.php
@@ -0,0 +1,894 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Plugin\views\field;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RedirectDestinationTrait;
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\Plugin\views\field\FieldPluginBase;
+use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait;
+use Drupal\views\Plugin\views\style\Table;
+use Drupal\views\ResultRow;
+use Drupal\views\ViewExecutable;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\views_bulk_operations\Service\ViewsbulkOperationsViewDataInterface;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager;
+use Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface;
+use Drupal\views_bulk_operations\Form\ViewsBulkOperationsFormTrait;
+use Drupal\user\PrivateTempStoreFactory;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Drupal\Core\Url;
+
+/**
+ * Defines the Views Bulk Operations field plugin.
+ *
+ * @ingroup views_field_handlers
+ *
+ * @ViewsField("views_bulk_operations_bulk_form")
+ */
+class ViewsBulkOperationsBulkForm extends FieldPluginBase implements CacheableDependencyInterface, ContainerFactoryPluginInterface {
+
+  use RedirectDestinationTrait;
+  use UncacheableFieldHandlerTrait;
+  use ViewsBulkOperationsFormTrait;
+
+  /**
+   * Object that gets the current view data.
+   *
+   * @var \Drupal\views_bulk_operations\ViewsbulkOperationsViewDataInterface
+   */
+  protected $viewData;
+
+  /**
+   * Views Bulk Operations action manager.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager
+   */
+  protected $actionManager;
+
+  /**
+   * Views Bulk Operations action processor.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface
+   */
+  protected $actionProcessor;
+
+  /**
+   * User private temporary storage factory.
+   *
+   * @var \Drupal\user\PrivateTempStoreFactory
+   */
+  protected $tempStoreFactory;
+
+  /**
+   * The current user object.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * An array of actions that can be executed.
+   *
+   * @var array
+   */
+  protected $actions = [];
+
+  /**
+   * An array of bulk form options.
+   *
+   * @var array
+   */
+  protected $bulkOptions;
+
+  /**
+   * Tempstore data.
+   *
+   * This gets passed to the next requests if needed
+   * or used in the views form submit handler directly.
+   *
+   * @var array
+   */
+  protected $tempStoreData = [];
+
+  /**
+   * Constructs a new BulkForm object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\views_bulk_operations\Service\ViewsbulkOperationsViewDataInterface $viewData
+   *   The VBO View Data provider service.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager $actionManager
+   *   Extended action manager object.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessorInterface $actionProcessor
+   *   Views Bulk Operations action processor.
+   * @param \Drupal\user\PrivateTempStoreFactory $tempStoreFactory
+   *   User private temporary storage factory.
+   * @param \Drupal\Core\Session\AccountInterface $currentUser
+   *   The current user object.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
+   *   The request stack.
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    ViewsbulkOperationsViewDataInterface $viewData,
+    ViewsBulkOperationsActionManager $actionManager,
+    ViewsBulkOperationsActionProcessorInterface $actionProcessor,
+    PrivateTempStoreFactory $tempStoreFactory,
+    AccountInterface $currentUser,
+    RequestStack $requestStack
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->viewData = $viewData;
+    $this->actionManager = $actionManager;
+    $this->actionProcessor = $actionProcessor;
+    $this->tempStoreFactory = $tempStoreFactory;
+    $this->currentUser = $currentUser;
+    $this->requestStack = $requestStack;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('views_bulk_operations.data'),
+      $container->get('plugin.manager.views_bulk_operations_action'),
+      $container->get('views_bulk_operations.processor'),
+      $container->get('user.private_tempstore'),
+      $container->get('current_user'),
+      $container->get('request_stack')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+    parent::init($view, $display, $options);
+
+    // Don't initialize if view has been built from VBO action processor.
+    if (!empty($this->view->views_bulk_operations_processor_built)) {
+      return;
+    }
+
+    // Set this property to always have the total rows information.
+    $this->view->get_total_rows = TRUE;
+
+    // Initialize VBO View Data object.
+    $this->viewData->init($view, $display, $this->options['relationship']);
+
+    // Fetch actions.
+    $this->actions = [];
+    $entity_types = $this->viewData->getEntityTypeIds();
+
+    // Get actions only if there are any entity types set for the view.
+    if (!empty($entity_types)) {
+      foreach ($this->actionManager->getDefinitions() as $id => $definition) {
+        if (empty($definition['type']) || in_array($definition['type'], $entity_types, TRUE)) {
+          $this->actions[$id] = $definition;
+        }
+      }
+    }
+
+    // Force form_step setting to TRUE due to #2879310.
+    $this->options['form_step'] = TRUE;
+  }
+
+  /**
+   * Update tempstore data.
+   *
+   * This function must be called a bit later, when the view
+   * query has been built. Also, no point doing this on the view
+   * admin page.
+   */
+  protected function updateTempstoreData() {
+    // Initialize tempstore object and get data if available.
+    $this->tempStoreData = $this->getTempstoreData($this->view->id(), $this->view->current_display);
+
+    // Parameters subject to change (either by an admin or user action).
+    $variable = [
+      'batch' => $this->options['batch'],
+      'batch_size' => $this->options['batch'] ? $this->options['batch_size'] : 0,
+      'total_results' => $this->viewData->getTotalResults(),
+      'arguments' => $this->view->args,
+      'redirect_url' => Url::createFromRequest(clone $this->requestStack->getCurrentRequest()),
+      'exposed_input' => $this->view->getExposedInput(),
+    ];
+
+    // Create tempstore data object if it doesn't exist.
+    if (!is_array($this->tempStoreData)) {
+      $this->tempStoreData = [];
+
+      // Add constant parameters.
+      $this->tempStoreData += [
+        'view_id' => $this->view->id(),
+        'display_id' => $this->view->current_display,
+        'list' => [],
+      ];
+
+      // Add variable parameters.
+      $this->tempStoreData += $variable;
+
+      $this->setTempstoreData($this->tempStoreData);
+    }
+
+    // Update some of the tempstore data parameters if required.
+    else {
+      $update = FALSE;
+
+      // Delete list if view arguments and optionally exposed filters changed.
+      // NOTE: this should be subject to a discussion, maybe tempstore
+      // should be arguments - specific?
+      $clear_triggers = ['arguments'];
+      if ($this->options['clear_on_exposed']) {
+        $clear_triggers[] = 'exposed_input';
+      }
+
+      foreach ($clear_triggers as $trigger) {
+        if ($variable[$trigger] !== $this->tempStoreData[$trigger]) {
+          $this->tempStoreData[$trigger] = $variable[$trigger];
+          $this->tempStoreData['list'] = [];
+        }
+        unset($variable[$trigger]);
+        $update = TRUE;
+      }
+
+      foreach ($variable as $param => $value) {
+        if (!isset($this->tempStoreData[$param]) || $this->tempStoreData[$param] != $value) {
+          $update = TRUE;
+          $this->tempStoreData[$param] = $value;
+        }
+      }
+
+      if ($update) {
+        $this->setTempstoreData($this->tempStoreData);
+      }
+    }
+
+  }
+
+  /**
+   * Gets the current user.
+   *
+   * @return \Drupal\Core\Session\AccountInterface
+   *   The current user.
+   */
+  protected function currentUser() {
+    return $this->currentUser;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    // @todo Consider making the bulk operation form cacheable. See
+    //   https://www.drupal.org/node/2503009.
+    return 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntity(ResultRow $row) {
+    return $this->viewData->getEntity($row);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions() {
+    $options = parent::defineOptions();
+    $options['batch'] = ['default' => TRUE];
+    $options['batch_size'] = ['default' => 10];
+    $options['form_step'] = ['default' => TRUE];
+    $options['buttons'] = ['default' => FALSE];
+    $options['clear_on_exposed'] = ['default' => FALSE];
+    $options['action_title'] = ['default' => $this->t('Action')];
+    $options['selected_actions'] = ['default' => []];
+    $options['preconfiguration'] = ['default' => []];
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+    // If the view type is not supported, suppress form display.
+    // Also display information note to the user.
+    if (empty($this->actions)) {
+      $form = [
+        '#type' => 'item',
+        '#title' => $this->t('NOTE'),
+        '#markup' => $this->t('Views Bulk Operations will work only with normal entity views and contrib module views that are integrated. See \Drupal\views_bulk_operations\EventSubscriber\ViewsBulkOperationsEventSubscriber class for integration best practice.'),
+        '#prefix' => '<div class="scroll">',
+        '#suffix' => '</div>',
+      ];
+      return;
+    }
+
+    $form['#attributes']['class'][] = 'views-bulk-operations-ui';
+    $form['#attached']['library'][] = 'views_bulk_operations/adminUi';
+
+    $form['batch'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Process in a batch operation'),
+      '#default_value' => $this->options['batch'],
+    ];
+
+    $form['batch_size'] = [
+      '#title' => $this->t('Batch size'),
+      '#type' => 'number',
+      '#min' => 1,
+      '#step' => 1,
+      '#description' => $this->t('Only applicable if results are processed in a batch operation.'),
+      '#default_value' => $this->options['batch_size'],
+    ];
+
+    $form['form_step'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Configuration form on new page (configurable actions)'),
+      '#default_value' => $this->options['form_step'],
+      // Due to #2879310 this setting must always be at TRUE.
+      '#access' => FALSE,
+    ];
+
+    $form['buttons'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Display selectable actions as buttons.'),
+      '#default_value' => $this->options['buttons'],
+    ];
+
+    $form['clear_on_exposed'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Clear selection when exposed filters change.'),
+      '#default_value' => $this->options['clear_on_exposed'],
+    ];
+
+    $form['action_title'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Action title'),
+      '#default_value' => $this->options['action_title'],
+      '#description' => $this->t('The title shown above the actions dropdown.'),
+    ];
+
+    $form['selected_actions'] = [
+      '#tree' => TRUE,
+      '#type' => 'details',
+      '#open' => TRUE,
+      '#title' => $this->t('Selected actions'),
+      '#attributes' => ['class' => ['vbo-actions-widget']],
+    ];
+
+    // Load values for display.
+    $form_values = $form_state->getValue(['options', 'selected_actions']);
+    if (is_null($form_values)) {
+      $selected_actions = $this->options['selected_actions'];
+      $preconfiguration = $this->options['preconfiguration'];
+    }
+    else {
+      $selected_actions = [];
+      $preconfiguration = [];
+      foreach ($form_values as $id => $value) {
+        $selected_actions[$id] = $value['state'] ? $id : 0;
+        $preconfiguration[$id] = isset($value['preconfiguration']) ? $value['preconfiguration'] : [];
+      }
+    }
+
+    foreach ($this->actions as $id => $action) {
+      $form['selected_actions'][$id]['state'] = [
+        '#type' => 'checkbox',
+        '#title' => $action['label'],
+        '#default_value' => empty($selected_actions[$id]) ? 0 : 1,
+        '#attributes' => ['class' => ['vbo-action-state']],
+      ];
+
+      // There are problems with AJAX on this form when adding
+      // new elements (Views issue), a workaround is to render
+      // all elements and show/hide them when needed.
+      $form['selected_actions'][$id]['preconfiguration'] = [
+        '#type' => 'fieldset',
+        '#title' => $this->t('Preconfiguration for "@action"', [
+          '@action' => $action['label'],
+        ]),
+        '#attributes' => [
+          'data-for' => $id,
+          'style' => empty($selected_actions[$id]) ? 'display: none' : NULL,
+        ],
+      ];
+
+      // Default label_override element.
+      $form['selected_actions'][$id]['preconfiguration']['label_override'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Override label'),
+        '#description' => $this->t('Leave empty for the default label.'),
+        '#default_value' => isset($preconfiguration[$id]['label_override']) ? $preconfiguration[$id]['label_override'] : '',
+      ];
+
+      // Load preconfiguration form if available.
+      if (method_exists($action['class'], 'buildPreConfigurationForm')) {
+        if (!isset($preconfiguration[$id])) {
+          $preconfiguration[$id] = [];
+        }
+        $actionObject = $this->actionManager->createInstance($id);
+        $form['selected_actions'][$id]['preconfiguration'] = $actionObject->buildPreConfigurationForm($form['selected_actions'][$id]['preconfiguration'], $preconfiguration[$id], $form_state);
+      }
+    }
+
+    parent::buildOptionsForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitOptionsForm(&$form, FormStateInterface $form_state) {
+    $options = &$form_state->getValue('options');
+    foreach ($options['selected_actions'] as $id => $action) {
+      if (!empty($action['state'])) {
+        if (isset($action['preconfiguration'])) {
+          $options['preconfiguration'][$id] = $action['preconfiguration'];
+          unset($options['selected_actions'][$id]['preconfiguration']);
+        }
+        $options['selected_actions'][$id] = $id;
+      }
+      else {
+        unset($options['preconfiguration'][$id]);
+        $options['selected_actions'][$id] = 0;
+      }
+    }
+    parent::submitOptionsForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preRender(&$values) {
+    parent::preRender($values);
+
+    // Add empty classes if there are no actions available.
+    if (empty($this->getBulkOptions())) {
+      $this->options['element_label_class'] .= 'empty';
+      $this->options['element_class'] .= 'empty';
+      $this->options['element_wrapper_class'] .= 'empty';
+      $this->options['label'] = '';
+    }
+    // If the view is using a table style, provide a placeholder for a
+    // "select all" checkbox.
+    elseif (!empty($this->view->style_plugin) && $this->view->style_plugin instanceof Table) {
+      // Add the tableselect css classes.
+      $this->options['element_label_class'] .= 'select-all';
+      // Hide the actual label of the field on the table header.
+      $this->options['label'] = '';
+    }
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue(ResultRow $row, $field = NULL) {
+    return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
+  }
+
+  /**
+   * Form constructor for the bulk form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function viewsForm(array &$form, FormStateInterface $form_state) {
+    // Make sure we do not accidentally cache this form.
+    // @todo Evaluate this again in https://www.drupal.org/node/2503009.
+    $form['#cache']['max-age'] = 0;
+
+    // Add VBO class to the form.
+    $form['#attributes']['class'][] = 'vbo-view-form';
+
+    // Add VBO front UI and tableselect libraries for table display style.
+    if ($this->view->style_plugin instanceof Table) {
+      $form['#attached']['library'][] = 'core/drupal.tableselect';
+      $this->view->style_plugin->options['views_bulk_operations_enabled'] = TRUE;
+    }
+    $form['#attached']['library'][] = 'views_bulk_operations/frontUi';
+    // Only add the bulk form options and buttons if
+    // there are results and any actions are available.
+    $action_options = $this->getBulkOptions();
+    if (!empty($this->view->result) && !empty($action_options)) {
+
+      // Update tempstore data.
+      $this->updateTempstoreData();
+
+      $form[$this->options['id']]['#tree'] = TRUE;
+
+      // Get pager data if available.
+      if (!empty($this->view->pager) && method_exists($this->view->pager, 'hasMoreRecords')) {
+        $pagerData = [
+          'current' => $this->view->pager->getCurrentPage(),
+          'more' => $this->view->pager->hasMoreRecords(),
+        ];
+      }
+
+      // Render checkboxes for all rows.
+      $page_selected = [];
+      $base_field = $this->view->storage->get('base_field');
+      foreach ($this->view->result as $row_index => $row) {
+        $entity = $this->getEntity($row);
+        $bulk_form_key = self::calculateEntityBulkFormKey(
+          $entity,
+          $row->{$base_field}
+        );
+
+        $checked = isset($this->tempStoreData['list'][$bulk_form_key]);
+        if ($checked) {
+          $page_selected[] = $bulk_form_key;
+        }
+        $form[$this->options['id']][$row_index] = [
+          '#type' => 'checkbox',
+          '#title' => $entity->label(),
+          '#title_display' => 'invisible',
+          '#default_value' => $checked,
+          '#return_value' => $bulk_form_key,
+        ];
+      }
+
+      // Ensure a consistent container for filters/operations
+      // in the view header.
+      $form['header'] = [
+        '#type' => 'container',
+        '#weight' => -100,
+      ];
+
+      // Build the bulk operations action widget for the header.
+      // Allow themes to apply .container-inline on this separate container.
+      $form['header'][$this->options['id']] = [
+        '#type' => 'container',
+        '#attributes' => [
+          'id' => 'vbo-action-form-wrapper',
+        ],
+      ];
+
+      // Display actions buttons or selector.
+      if ($this->options['buttons']) {
+        unset($form['actions']['submit']);
+        foreach ($action_options as $id => $label) {
+          $form['actions'][$id] = [
+            '#type' => 'submit',
+            '#value' => $label,
+          ];
+        }
+      }
+      else {
+        // Replace the form submit button label.
+        $form['actions']['submit']['#value'] = $this->t('Apply to selected items');
+
+        $form['header'][$this->options['id']]['action'] = [
+          '#type' => 'select',
+          '#title' => $this->options['action_title'],
+          '#options' => ['' => $this->t('-- Select action --')] + $action_options,
+        ];
+      }
+
+      // Add AJAX functionality if actions are configurable through this form.
+      if (empty($this->options['form_step'])) {
+        $form['header'][$this->options['id']]['action']['#ajax'] = [
+          'callback' => [__CLASS__, 'viewsFormAjax'],
+          'wrapper' => 'vbo-action-configuration-wrapper',
+        ];
+        $form['header'][$this->options['id']]['configuration'] = [
+          '#type' => 'container',
+          '#attributes' => ['id' => 'vbo-action-configuration-wrapper'],
+        ];
+
+        $action_id = $form_state->getValue('action');
+        if (!empty($action_id)) {
+          $action = $this->actions[$action_id];
+          if ($this->isConfigurable($action)) {
+            $actionObject = $this->actionManager->createInstance($action_id);
+            $form['header'][$this->options['id']]['configuration'] += $actionObject->buildConfigurationForm($form['header'][$this->options['id']]['configuration'], $form_state);
+            $form['header'][$this->options['id']]['configuration']['#config_included'] = TRUE;
+          }
+        }
+      }
+
+      $display_select_all = isset($pagerData) && ($pagerData['more'] || $pagerData['current'] > 0);
+      // Selection info: displayed if exposed filters are set and selection
+      // is not cleared when they change or "select all" element display
+      // conditions are met.
+      if ((!$this->options['clear_on_exposed'] && !empty($this->view->getExposedInput())) || $display_select_all) {
+
+        $form['header'][$this->options['id']]['multipage'] = [
+          '#type' => 'details',
+          '#open' => FALSE,
+          '#title' => $this->t('Selected %count items in this view', [
+            '%count' => count($this->tempStoreData['list']),
+          ]),
+          '#attributes' => [
+            // Add view_id and display_id to be available for
+            // js multipage selector functionality.
+            'data-view-id' => $this->tempStoreData['view_id'],
+            'data-display-id' => $this->tempStoreData['display_id'],
+            'class' => ['vbo-multipage-selector'],
+            'name' => 'somename',
+          ],
+        ];
+
+        // Display a list of items selected on other pages.
+        $form['header'][$this->options['id']]['multipage']['list'] = [
+          '#theme' => 'item_list',
+          '#title' => $this->t('Items selected on other pages:'),
+          '#items' => [],
+          '#empty' => $this->t('No selection'),
+        ];
+        if (count($this->tempStoreData['list']) > count($page_selected)) {
+          foreach ($this->tempStoreData['list'] as $bulk_form_key => $item) {
+            if (!in_array($bulk_form_key, $page_selected)) {
+              $form['header'][$this->options['id']]['multipage']['list']['#items'][] = $item[4];
+            }
+          }
+          $form['header'][$this->options['id']]['multipage']['clear'] = [
+            '#type' => 'submit',
+            '#value' => $this->t('Clear'),
+            '#submit' => [[$this, 'clearSelection']],
+            '#limit_validation_errors' => [],
+          ];
+        }
+      }
+
+      // Select all results checkbox.
+      if ($display_select_all) {
+        $form['header'][$this->options['id']]['select_all'] = [
+          '#type' => 'checkbox',
+          '#title' => $this->t('Select all@count results in this view', [
+            '@count' => $this->tempStoreData['total_results'] ? ' ' . $this->tempStoreData['total_results'] : '',
+          ]),
+          '#attributes' => ['class' => ['vbo-select-all']],
+        ];
+      }
+
+      // Duplicate the form actions into the action container in the header.
+      $form['header'][$this->options['id']]['actions'] = $form['actions'];
+    }
+    else {
+      // Remove the default actions build array.
+      unset($form['actions']);
+    }
+
+  }
+
+  /**
+   * AJAX callback for the views form.
+   *
+   * Currently not used due to #2879310.
+   */
+  public static function viewsFormAjax(array $form, FormStateInterface $form_state) {
+    $trigger = $form_state->getTriggeringElement();
+    $plugin_id = $trigger['#array_parents'][1];
+    return $form['header'][$plugin_id]['configuration'];
+  }
+
+  /**
+   * Returns the available operations for this form.
+   *
+   * @return array
+   *   An associative array of operations, suitable for a select element.
+   */
+  protected function getBulkOptions() {
+    if (!isset($this->bulkOptions)) {
+      $this->bulkOptions = [];
+      foreach ($this->actions as $id => $definition) {
+        // Filter out actions that weren't selected.
+        if (!in_array($id, $this->options['selected_actions'], TRUE)) {
+          continue;
+        }
+
+        // Check access permission, if defined.
+        if (!empty($definition['requirements']['_permission']) && !$this->currentUser->hasPermission($definition['requirements']['_permission'])) {
+          continue;
+        }
+
+        // Override label if applicable.
+        if (!empty($this->options['preconfiguration'][$id]['label_override'])) {
+          $this->bulkOptions[$id] = $this->options['preconfiguration'][$id]['label_override'];
+        }
+        else {
+          $this->bulkOptions[$id] = $definition['label'];
+        }
+      }
+    }
+
+    return $this->bulkOptions;
+  }
+
+  /**
+   * Submit handler for the bulk form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function viewsFormSubmit(array &$form, FormStateInterface $form_state) {
+    if ($form_state->get('step') == 'views_form_views_form') {
+
+      $action_id = $form_state->getValue('action');
+
+      $action = $this->actions[$action_id];
+
+      $this->tempStoreData['action_id'] = $action_id;
+      $this->tempStoreData['action_label'] = empty($this->options['preconfiguration'][$action_id]['label_override']) ? (string) $action['label'] : $this->options['preconfiguration'][$action_id]['label_override'];
+      $this->tempStoreData['relationship_id'] = $this->options['relationship'];
+      $this->tempStoreData['preconfiguration'] = isset($this->options['preconfiguration'][$action_id]) ? $this->options['preconfiguration'][$action_id] : [];
+
+      if (!$form_state->getValue('select_all')) {
+
+        // Update list data with the current form selection.
+        foreach ($form_state->getValue($this->options['id']) as $row_index => $bulkFormKey) {
+          if ($bulkFormKey) {
+            $this->tempStoreData['list'][$bulkFormKey] = $this->getListItem($bulkFormKey, $form[$this->options['id']][$row_index]['#title']);
+          }
+          else {
+            unset($this->tempStoreData['list'][$form[$this->options['id']][$row_index]['#return_value']]);
+          }
+        }
+      }
+      else {
+        // Unset the list completely.
+        $this->tempStoreData['list'] = [];
+      }
+
+      $configurable = $this->isConfigurable($action);
+
+      // Get configuration if using AJAX.
+      if ($configurable && empty($this->options['form_step'])) {
+        $actionObject = $this->actionManager->createInstance($action_id);
+        if (method_exists($actionObject, 'submitConfigurationForm')) {
+          $actionObject->submitConfigurationForm($form, $form_state);
+          $this->tempStoreData['configuration'] = $actionObject->getConfiguration();
+        }
+        else {
+          $form_state->cleanValues();
+          $this->tempStoreData['configuration'] = $form_state->getValues();
+        }
+      }
+
+      // Routing - determine redirect route.
+      if ($this->options['form_step'] && $configurable) {
+        $redirect_route = 'views_bulk_operations.execute_configurable';
+      }
+      elseif ($this->options['batch']) {
+        if (!empty($action['confirm_form_route_name'])) {
+          $redirect_route = $action['confirm_form_route_name'];
+        }
+      }
+      elseif (!empty($action['confirm_form_route_name'])) {
+        $redirect_route = $action['confirm_form_route_name'];
+      }
+
+      // Redirect if needed.
+      if (!empty($redirect_route)) {
+        $this->setTempstoreData($this->tempStoreData);
+
+        $form_state->setRedirect($redirect_route, [
+          'view_id' => $this->view->id(),
+          'display_id' => $this->view->current_display,
+        ]);
+      }
+      // Or process rows here and now.
+      else {
+        $this->deleteTempstoreData();
+        $this->actionProcessor->executeProcessing($this->tempStoreData, $this->view);
+        $form_state->setRedirectUrl($this->tempStoreData['redirect_url']);
+      }
+    }
+  }
+
+  /**
+   * Clear the form selection along with entire tempstore.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function clearSelection(array &$form, FormStateInterface $form_state) {
+    $this->deleteTempstoreData();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewsFormValidate(&$form, FormStateInterface $form_state) {
+    if ($this->options['buttons']) {
+      $trigger = $form_state->getTriggeringElement();
+      $action_id = end($trigger['#parents']);
+      $form_state->setValue('action', $action_id);
+    }
+
+    if (empty($form_state->getValue('action'))) {
+      $form_state->setErrorByName('action', $this->t('Please select an action to perform.'));
+    }
+
+    // This happened once, can't reproduce but here's a safety switch.
+    if (!isset($this->actions[$form_state->getValue('action')])) {
+      $form_state->setErrorByName('action', $this->t('Form error occurred, please try again.'));
+    }
+
+    if (!$form_state->getValue('select_all')) {
+      // Update tempstore data to make sure we have also
+      // results selected in other requests and validate if
+      // anything is selected.
+      $this->tempStoreData = $this->getTempstoreData();
+      $selected = array_filter($form_state->getValue($this->options['id']));
+      if (empty($this->tempStoreData['list']) && empty($selected)) {
+        $form_state->setErrorByName('', $this->t('No items selected.'));
+      }
+    }
+
+    // Action config validation (if implemented).
+    if (empty($this->options['form_step']) && !empty($form['header'][$this->options['id']]['configuration']['#config_included'])) {
+      $action_id = $form_state->getValue('action');
+      $action = $this->actions[$action_id];
+      if (method_exists($action['class'], 'validateConfigurationForm')) {
+        $actionObject = $this->actionManager->createInstance($action_id);
+        $actionObject->validateConfigurationForm($form['header'][$this->options['id']]['configuration'], $form_state);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clickSortable() {
+    return FALSE;
+  }
+
+  /**
+   * Check if an action is configurable.
+   */
+  protected function isConfigurable($action) {
+    return (in_array('Drupal\Core\Plugin\PluginFormInterface', class_implements($action['class']), TRUE) || method_exists($action['class'], 'buildConfigurationForm'));
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionManager.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..39fa43e004a26be7d18546c579df3667d9b9c646
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionManager.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Service;
+
+use Drupal\Core\Action\ActionManager;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+
+/**
+ * Defines Views Bulk Operations action manager.
+ *
+ * Extends the core Action Manager to allow VBO actions
+ * define additional configuration.
+ */
+class ViewsBulkOperationsActionManager extends ActionManager {
+
+  const ALTER_ACTIONS_EVENT = 'views_bulk_operations.action_definitions';
+
+  /**
+   * Event dispatcher service.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * Additional parameters passed to alter event.
+   *
+   * @var array
+   */
+  protected $alterParameters;
+
+  /**
+   * Service constructor.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   The module handler to invoke the alter hook with.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+   *   The event dispatcher service.
+   */
+  public function __construct(
+    \Traversable $namespaces,
+    CacheBackendInterface $cacheBackend,
+    ModuleHandlerInterface $moduleHandler,
+    EventDispatcherInterface $eventDispatcher
+  ) {
+    parent::__construct($namespaces, $cacheBackend, $moduleHandler);
+    $this->eventDispatcher = $eventDispatcher;
+    $this->setCacheBackend($cacheBackend, 'views_bulk_operations_action_info');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function findDefinitions() {
+    $definitions = $this->getDiscovery()->getDefinitions();
+
+    // Incompatible actions.
+    $incompatible = [
+      'node_delete_action',
+      'user_cancel_user_action',
+    ];
+
+    foreach ($definitions as $plugin_id => &$definition) {
+      $this->processDefinition($definition, $plugin_id);
+      if (empty($definition) || in_array($definition['id'], $incompatible)) {
+        unset($definitions[$plugin_id]);
+      }
+    }
+    $this->alterDefinitions($definitions);
+    foreach ($definitions as $plugin_id => $plugin_definition) {
+      // If the plugin definition is an object, attempt to convert it to an
+      // array, if that is not possible, skip further processing.
+      if (is_object($plugin_definition) && !($plugin_definition = (array) $plugin_definition)) {
+        continue;
+      }
+      // If this plugin was provided by a module that does not exist, remove the
+      // plugin definition.
+      if (isset($plugin_definition['provider']) && !in_array($plugin_definition['provider'], ['core', 'component']) && !$this->providerExists($plugin_definition['provider'])) {
+        unset($definitions[$plugin_id]);
+      }
+    }
+    return $definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @param array $parameters
+   *   Parameters of the method. Passed to alter event.
+   */
+  public function getDefinitions(array $parameters = []) {
+    if (empty($parameters['nocache'])) {
+      $definitions = $this->getCachedDefinitions();
+    }
+    if (!isset($definitions)) {
+      $this->alterParameters = $parameters;
+      $definitions = $this->findDefinitions($parameters);
+
+      $this->setCachedDefinitions($definitions);
+    }
+
+    return $definitions;
+  }
+
+  /**
+   * Gets a specific plugin definition.
+   *
+   * @param string $plugin_id
+   *   A plugin id.
+   * @param bool $exception_on_invalid
+   *   (optional) If TRUE, an invalid plugin ID will throw an exception.
+   * @param array $parameters
+   *   Parameters of the method. Passed to alter event.
+   *
+   * @return mixed
+   *   A plugin definition, or NULL if the plugin ID is invalid and
+   *   $exception_on_invalid is FALSE.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+   *   Thrown if $plugin_id is invalid and $exception_on_invalid is TRUE.
+   */
+  public function getDefinition($plugin_id, $exception_on_invalid = TRUE, array $parameters = []) {
+    // Loading all definitions here will not hurt much, as they're cached,
+    // and we need the option to alter a definition.
+    $definitions = $this->getDefinitions($parameters);
+    if (isset($definitions[$plugin_id])) {
+      return $definitions[$plugin_id];
+    }
+    elseif (!$exception_on_invalid) {
+      return NULL;
+    }
+
+    throw new PluginNotFoundException($plugin_id, sprintf('The "%s" plugin does not exist.', $plugin_id));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    // Only arrays can be operated on.
+    if (!is_array($definition)) {
+      return;
+    }
+
+    if (!empty($this->defaults) && is_array($this->defaults)) {
+      $definition = NestedArray::mergeDeep($this->defaults, $definition);
+    }
+
+    // Merge in defaults.
+    $definition += [
+      'confirm' => FALSE,
+    ];
+
+    // Add default confirmation form if confirm set to TRUE
+    // and not explicitly set.
+    if ($definition['confirm'] && empty($definition['confirm_form_route_name'])) {
+      $definition['confirm_form_route_name'] = 'views_bulk_operations.confirm';
+    }
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function alterDefinitions(&$definitions) {
+    // Let other modules change definitions.
+    // Main purpose: Action permissions bridge.
+    $event = new Event();
+    $event->alterParameters = $this->alterParameters;
+    $event->definitions = &$definitions;
+    $this->eventDispatcher->dispatch(static::ALTER_ACTIONS_EVENT, $event);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessor.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessor.php
new file mode 100644
index 0000000000000000000000000000000000000000..cb9d7cb328a29438787c295834043385f0c94cb4
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessor.php
@@ -0,0 +1,419 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Service;
+
+use Drupal\views\Views;
+use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\views_bulk_operations\ViewsBulkOperationsBatch;
+
+/**
+ * Defines VBO action processor.
+ */
+class ViewsBulkOperationsActionProcessor implements ViewsBulkOperationsActionProcessorInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * View data provider service.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsbulkOperationsViewDataInterface
+   */
+  protected $viewDataService;
+
+  /**
+   * VBO action manager.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager
+   */
+  protected $actionManager;
+
+  /**
+   * Current user object.
+   *
+   * @var \Drupal\Core\Session\AccountProxyInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Is the object initialized?
+   *
+   * @var bool
+   */
+  protected $initialized = FALSE;
+
+  /**
+   * The processed action object.
+   *
+   * @var array
+   */
+  protected $action;
+
+  /**
+   * The current view object.
+   *
+   * @var \Drupal\views\ViewExecutable
+   */
+  protected $view;
+
+  /**
+   * View data from the bulk form.
+   *
+   * @var array
+   */
+  protected $bulkFormData;
+
+  /**
+   * Array of entities that will be processed in the current batch.
+   *
+   * @var array
+   */
+  protected $queue = [];
+
+  /**
+   * Constructor.
+   *
+   * @param \Drupal\views_bulk_operations\Service\ViewsbulkOperationsViewDataInterface $viewDataService
+   *   View data provider service.
+   * @param \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager $actionManager
+   *   VBO action manager.
+   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
+   *   Current user object.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   Module handler service.
+   */
+  public function __construct(
+    ViewsbulkOperationsViewDataInterface $viewDataService,
+    ViewsBulkOperationsActionManager $actionManager,
+    AccountProxyInterface $currentUser,
+    ModuleHandlerInterface $moduleHandler
+  ) {
+    $this->viewDataService = $viewDataService;
+    $this->actionManager = $actionManager;
+    $this->currentUser = $currentUser;
+    $this->moduleHandler = $moduleHandler;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function initialize(array $view_data, $view = NULL) {
+
+    // It may happen that the service was already initialized
+    // in this request (e.g. multiple Batch API operation calls).
+    // Clear the processing queue in such a case.
+    if ($this->initialized) {
+      $this->queue = [];
+    }
+
+    if (!isset($view_data['configuration'])) {
+      $view_data['configuration'] = [];
+    }
+    if (!empty($view_data['preconfiguration'])) {
+      $view_data['configuration'] += $view_data['preconfiguration'];
+    }
+
+    // Initialize action object.
+    $this->action = $this->actionManager->createInstance($view_data['action_id'], $view_data['configuration']);
+
+    // Set action context.
+    $this->setActionContext($view_data);
+
+    // Set entire view data as object parameter for future reference.
+    $this->bulkFormData = $view_data;
+
+    // Set the current view.
+    $this->setView($view);
+
+    $this->initialized = TRUE;
+  }
+
+  /**
+   * Set the current view object.
+   *
+   * @param mixed $view
+   *   The current view object or NULL.
+   */
+  protected function setView($view = NULL) {
+    if (!is_null($view)) {
+      $this->view = $view;
+    }
+    else {
+      $this->view = Views::getView($this->bulkFormData['view_id']);
+      $this->view->setDisplay($this->bulkFormData['display_id']);
+    }
+    $this->view->get_total_rows = TRUE;
+    $this->view->views_bulk_operations_processor_built = TRUE;
+    if (!empty($this->bulkFormData['arguments'])) {
+      $this->view->setArguments($this->bulkFormData['arguments']);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPageList($page) {
+    $list = [];
+
+    $this->viewDataService->init($this->view, $this->view->getDisplay(), $this->bulkFormData['relationship_id']);
+
+    // Set exposed filters and pager parameters.
+    if (!empty($this->bulkFormData['exposed_input'])) {
+      $this->view->setExposedInput($this->bulkFormData['exposed_input']);
+    }
+    $this->view->setItemsPerPage($this->bulkFormData['batch_size']);
+    $this->view->setCurrentPage($page);
+    $this->view->build();
+
+    $offset = $this->bulkFormData['batch_size'] * $page;
+    // If the view doesn't start from the first result,
+    // move the offset.
+    if ($pager_offset = $this->view->pager->getOffset()) {
+      $offset += $pager_offset;
+    }
+    $this->view->query->setLimit($this->bulkFormData['batch_size']);
+    $this->view->query->setOffset($offset);
+    $this->moduleHandler->invokeAll('views_pre_execute', [$this->view]);
+    $this->view->query->execute($this->view);
+
+    $base_field = $this->view->storage->get('base_field');
+    foreach ($this->view->result as $row) {
+      $entity = $this->viewDataService->getEntity($row);
+
+      // We don't need entity label here.
+      $list[] = [
+        $row->{$base_field},
+        $entity->language()->getId(),
+        $entity->getEntityTypeId(),
+        $entity->id(),
+      ];
+    }
+
+    return $list;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function populateQueue(array $list, array &$context = []) {
+    // Determine batch size and offset.
+    if (!empty($context)) {
+      $batch_size = $this->bulkFormData['batch_size'];
+      if (!isset($context['sandbox']['current_batch'])) {
+        $context['sandbox']['current_batch'] = 0;
+      }
+      $current_batch = &$context['sandbox']['current_batch'];
+      $offset = $current_batch * $batch_size;
+    }
+    else {
+      $batch_size = 0;
+      $current_batch = 0;
+      $offset = 0;
+    }
+
+    if ($batch_size) {
+      $batch_list = array_slice($list, $offset, $batch_size);
+    }
+    else {
+      $batch_list = $list;
+    }
+
+    $base_field_values = [];
+    foreach ($batch_list as $item) {
+      $base_field_values[] = $item[0];
+    }
+    if (empty($base_field_values)) {
+      return 0;
+    }
+
+    $this->view->setItemsPerPage(0);
+    $this->view->setCurrentPage(0);
+    $this->view->setOffset(0);
+    $this->view->initHandlers();
+
+    // Remove all exposed filters so we don't have any default filter
+    // values that could make the actual selection out of range.
+    if (!empty($this->view->filter)) {
+      foreach ($this->view->filter as $id => $filter) {
+        if (!empty($filter->options['exposed'])) {
+          unset($this->view->filter[$id]);
+        }
+      }
+    }
+
+    // Build the view query.
+    $this->view->build();
+
+    // Modify the view query: determine and apply the base field condition.
+    $base_field = $this->view->storage->get('base_field');
+    if (isset($this->view->query->fields[$base_field])) {
+      $base_field_alias = $this->view->query->fields[$base_field]['table'] . '.' . $this->view->query->fields[$base_field]['alias'];
+    }
+    else {
+      $base_field_alias = $base_field;
+    }
+    $this->view->query->addWhere(0, $base_field_alias, $base_field_values, 'IN');
+
+    // Rebuild the view query.
+    $this->view->query->build($this->view);
+
+    // Execute the view.
+    $this->moduleHandler->invokeAll('views_pre_execute', [$this->view]);
+    $this->view->query->execute($this->view);
+
+    // Get entities.
+    $this->viewDataService->init($this->view, $this->view->getDisplay(), $this->bulkFormData['relationship_id']);
+    foreach ($this->view->result as $row_index => $row) {
+      // This may return rows for all possible languages.
+      // Check if the current language is on the list.
+      $found = FALSE;
+      $entity = $this->viewDataService->getEntity($row);
+      foreach ($batch_list as $delta => $item) {
+        if ($row->{$base_field} === $item[0] && $entity->language()->getId() === $item[1]) {
+          $this->queue[] = $entity;
+          $found = TRUE;
+          unset($batch_list[$delta]);
+          break;
+        }
+      }
+      if (!$found) {
+        unset($this->view->result[$row_index]);
+      }
+    }
+
+    // Extra processing when executed in a Batch API operation.
+    if (!empty($context)) {
+      if (!isset($context['sandbox']['total'])) {
+        if (empty($list)) {
+          $context['sandbox']['total'] = $this->viewDataService->getTotalResults();
+        }
+        else {
+          $context['sandbox']['total'] = count($list);
+        }
+      }
+      // Add batch size to context array for potential use in actions.
+      $context['sandbox']['batch_size'] = $batch_size;
+      $this->setActionContext($context);
+    }
+
+    if ($batch_size) {
+      $current_batch++;
+    }
+
+    $this->setActionView();
+
+    return count($this->queue);
+  }
+
+  /**
+   * Set action context if action method exists.
+   *
+   * @param array $context
+   *   The context to be set.
+   */
+  protected function setActionContext(array $context) {
+    if (isset($this->action) && method_exists($this->action, 'setContext')) {
+      $this->action->setContext($context);
+    }
+  }
+
+  /**
+   * Sets the current view object as the executed action parameter.
+   */
+  protected function setActionView() {
+    if (isset($this->action) && method_exists($this->action, 'setView')) {
+      $this->action->setView($this->view);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process() {
+    $output = [];
+
+    // Check if all queue items are actually Drupal entities.
+    foreach ($this->queue as $delta => $entity) {
+      if (!($entity instanceof EntityInterface)) {
+        $output[] = $this->t('Skipped');
+        unset($this->queue[$delta]);
+      }
+    }
+
+    // Check entity type for multi-type views like search_api index.
+    $action_definition = $this->actionManager->getDefinition($this->bulkFormData['action_id']);
+    if (!empty($action_definition['type'])) {
+      foreach ($this->queue as $delta => $entity) {
+        if ($entity->getEntityTypeId() !== $action_definition['type']) {
+          $output[] = $this->t('Entity type not supported');
+          unset($this->queue[$delta]);
+        }
+      }
+    }
+
+    // Check access.
+    foreach ($this->queue as $delta => $entity) {
+      if (!$this->action->access($entity, $this->currentUser)) {
+        $output[] = $this->t('Access denied');
+        unset($this->queue[$delta]);
+      }
+    }
+
+    // Process queue.
+    $results = $this->action->executeMultiple($this->queue);
+
+    // Populate output.
+    if (empty($results)) {
+      $count = count($this->queue);
+      for ($i = 0; $i < $count; $i++) {
+        $output[] = $this->bulkFormData['action_label'];
+      }
+    }
+    else {
+      foreach ($results as $result) {
+        $output[] = $result;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeProcessing(array &$data, $view = NULL) {
+    if ($data['batch']) {
+      $batch = ViewsBulkOperationsBatch::getBatch($data);
+      batch_set($batch);
+    }
+    else {
+      $list = $data['list'];
+
+      // Populate and process queue.
+      if (!$this->initialized) {
+        $this->initialize($data, $view);
+      }
+      if (empty($list)) {
+        $list = $this->getPageList(0);
+      }
+      if ($this->populateQueue($list)) {
+        $batch_results = $this->process();
+      }
+
+      $results = ['operations' => []];
+      foreach ($batch_results as $result) {
+        $results['operations'][] = (string) $result;
+      }
+      ViewsBulkOperationsBatch::finished(TRUE, $results, []);
+    }
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessorInterface.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessorInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..219cc11fb55ff3b882d6b65c9eb21361375c6d3e
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsActionProcessorInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Service;
+
+/**
+ * Defines Views Bulk Operations action processor.
+ */
+interface ViewsBulkOperationsActionProcessorInterface {
+
+  /**
+   * Set values.
+   *
+   * @param array $view_data
+   *   Data concerning the view that will be processed.
+   * @param mixed $view
+   *   The current view object or NULL.
+   */
+  public function initialize(array $view_data, $view = NULL);
+
+  /**
+   * Get full list of items from a specific view page.
+   *
+   * @param int $page
+   *   Results page number.
+   *
+   * @return array
+   *   Array of result data arrays.
+   */
+  public function getPageList($page);
+
+  /**
+   * Populate entity queue for processing.
+   *
+   * @param array $list
+   *   Array of selected view results.
+   * @param array $context
+   *   Batch API context.
+   */
+  public function populateQueue(array $list, array &$context = []);
+
+  /**
+   * Process results.
+   */
+  public function process();
+
+  /**
+   * Helper function for processing results from view data.
+   *
+   * @param array $data
+   *   Data concerning the view that will be processed.
+   * @param mixed $view
+   *   The current view object or NULL.
+   */
+  public function executeProcessing(array &$data, $view = NULL);
+
+}
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewData.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewData.php
new file mode 100644
index 0000000000000000000000000000000000000000..bc41f82081ff409bf1b37fc9a689d6409350ea8f
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewData.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Service;
+
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Drupal\views\ViewExecutable;
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\Views;
+use Drupal\views\ResultRow;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\views_bulk_operations\ViewsBulkOperationsEvent;
+
+/**
+ * Gets Views data needed by VBO.
+ */
+class ViewsBulkOperationsViewData implements ViewsBulkOperationsViewDataInterface {
+
+  /**
+   * Event dispatcher service.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The current view.
+   *
+   * @var \Drupal\views\ViewExecutable
+   */
+  protected $view;
+
+  /**
+   * The realtionship ID.
+   *
+   * @var string
+   */
+  protected $relationship;
+
+  /**
+   * Views data concerning the current view.
+   *
+   * @var array
+   */
+  protected $data;
+
+  /**
+   * Entity type ids returned by this view.
+   *
+   * @var array
+   */
+  protected $entityTypeIds;
+
+  /**
+   * Entity getter data.
+   *
+   * @var array
+   */
+  protected $entityGetter;
+
+  /**
+   * Object constructor.
+   *
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
+   *   The event dispatcher service.
+   */
+  public function __construct(EventDispatcherInterface $eventDispatcher) {
+    $this->eventDispatcher = $eventDispatcher;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init(ViewExecutable $view, DisplayPluginBase $display, $relationship) {
+    $this->view = $view;
+    $this->displayHandler = $display;
+    $this->relationship = $relationship;
+
+    // Get view entity types and results fetcher callable.
+    $event = new ViewsBulkOperationsEvent($this->getViewProvider(), $this->getData(), $view);
+    $this->eventDispatcher->dispatch(ViewsBulkOperationsEvent::NAME, $event);
+    $this->entityTypeIds = $event->getEntityTypeIds();
+    $this->entityGetter = $event->getEntityGetter();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeIds() {
+    return $this->entityTypeIds;
+  }
+
+  /**
+   * Helper function to get data of the current view.
+   *
+   * @return array
+   *   Part of views data that refers to the current view.
+   */
+  protected function getData() {
+    if (!$this->data) {
+      $viewsData = Views::viewsData();
+      if (!empty($this->relationship) && $this->relationship != 'none') {
+        $relationship = $this->displayHandler->getOption('relationships')[$this->relationship];
+        $table_data = $viewsData->get($relationship['table']);
+        $this->data = $viewsData->get($table_data[$relationship['field']]['relationship']['base']);
+      }
+      else {
+        $this->data = $viewsData->get($this->view->storage->get('base_table'));
+      }
+    }
+    return $this->data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewProvider() {
+    $views_data = $this->getData();
+    if (isset($views_data['table']['provider'])) {
+      return $views_data['table']['provider'];
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewBaseField() {
+    $views_data = $this->getData();
+    if (isset($views_data['table']['base']['field'])) {
+      return $views_data['table']['base']['field'];
+    }
+    throw new \Exception('Unable to get base field for the view.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntity(ResultRow $row) {
+    if (!empty($this->entityGetter['file'])) {
+      require_once $this->entityGetter['file'];
+    }
+    if (is_callable($this->entityGetter['callable'])) {
+      return call_user_func($this->entityGetter['callable'], $row, $this->relationship, $this->view);
+    }
+    else {
+      if (is_array($this->entityGetter['callable'])) {
+        if (is_object($this->entityGetter['callable'][0])) {
+          $info = get_class($this->entityGetter['callable'][0]);
+        }
+        else {
+          $info = $this->entityGetter['callable'][0];
+        }
+        $info .= '::' . $this->entityGetter['callable'][1];
+      }
+      else {
+        $info = $this->entityGetter['callable'];
+      }
+      throw new \Exception(sprintf("Entity getter method %s doesn't exist.", $info));
+    }
+  }
+
+  /**
+   * Get the total count of results on all pages.
+   *
+   * @return int
+   *   The total number of results this view displays.
+   */
+  public function getTotalResults() {
+    $total_results = NULL;
+    if (!empty($this->view->pager->total_items)) {
+      $total_results = $this->view->pager->total_items;
+    }
+    elseif (!empty($this->view->total_rows)) {
+      $total_results = $this->view->total_rows;
+    }
+
+    return $total_results;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityDefault(ResultRow $row, $relationship_id, ViewExecutable $view) {
+    if ($relationship_id == 'none') {
+      if (!empty($row->_entity)) {
+        $entity = $row->_entity;
+      }
+    }
+    elseif (isset($row->_relationship_entities[$relationship_id])) {
+      $entity = $row->_relationship_entities[$relationship_id];
+    }
+    else {
+      throw new \Exception('Unexpected view result row structure.');
+    }
+
+    if ($entity instanceof TranslatableInterface && $entity->isTranslatable()) {
+
+      // Try to find a field alias for the langcode.
+      // Assumption: translatable entities always
+      // have a langcode key.
+      $language_field = '';
+      $langcode_key = $entity->getEntityType()->getKey('langcode');
+      $base_table = $view->storage->get('base_table');
+      foreach ($view->query->fields as $field) {
+        if (
+          $field['field'] === $langcode_key && (
+            empty($field['base_table']) ||
+            $field['base_table'] === $base_table
+          )
+        ) {
+          $language_field = $field['alias'];
+          break;
+        }
+      }
+      if (!$language_field) {
+        $language_field = $langcode_key;
+      }
+
+      if (isset($row->{$language_field})) {
+        return $entity->getTranslation($row->{$language_field});
+      }
+    }
+
+    return $entity;
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewDataInterface.php b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewDataInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..ed41b78514b8132573fdd9c17fd4cd15395c7e8f
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/Service/ViewsBulkOperationsViewDataInterface.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\views_bulk_operations\Service;
+
+use Drupal\views\ViewExecutable;
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\ResultRow;
+
+/**
+ * Defines view data service for Views Bulk Operations.
+ */
+interface ViewsBulkOperationsViewDataInterface {
+
+  /**
+   * Initialize additional variables.
+   *
+   * @param \Drupal\views\ViewExecutable $view
+   *   The view object.
+   * @param \Drupal\views\Plugin\views\display\DisplayPluginBase $display
+   *   The current display plugin.
+   * @param string $relationship
+   *   Relationship ID.
+   */
+  public function init(ViewExecutable $view, DisplayPluginBase $display, $relationship);
+
+  /**
+   * Get entity type IDs.
+   *
+   * @return array
+   *   Array of entity type IDs.
+   */
+  public function getEntityTypeIds();
+
+  /**
+   * Get view provider.
+   *
+   * @return string
+   *   View provider ID.
+   */
+  public function getViewProvider();
+
+  /**
+   * Get base field for the current view.
+   *
+   * @return sting
+   *   The base field name.
+   */
+  public function getViewBaseField();
+
+  /**
+   * Get entity from views row.
+   *
+   * @param \Drupal\views\ResultRow $row
+   *   Views row object.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   An entity object.
+   */
+  public function getEntity(ResultRow $row);
+
+  /**
+   * Get the total count of results on all pages.
+   *
+   * @return int
+   *   The total number of results this view displays.
+   */
+  public function getTotalResults();
+
+  /**
+   * The default entity getter function.
+   *
+   * Must work well with standard Drupal core entity views.
+   *
+   * @param \Drupal\views\ResultRow $row
+   *   Views result row.
+   * @param string $relationship_id
+   *   Id of the view relationship.
+   * @param \Drupal\views\ViewExecutable $view
+   *   The current view object.
+   *
+   * @return \Drupal\Core\Entity\FieldableEntityInterface
+   *   The translated entity.
+   */
+  public function getEntityDefault(ResultRow $row, $relationship_id, ViewExecutable $view);
+
+}
diff --git a/web/modules/views_bulk_operations/src/ViewsBulkOperationsBatch.php b/web/modules/views_bulk_operations/src/ViewsBulkOperationsBatch.php
new file mode 100644
index 0000000000000000000000000000000000000000..f43f7ebc8af54cbdbef88d94ab3d4dc70e64fb85
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/ViewsBulkOperationsBatch.php
@@ -0,0 +1,233 @@
+<?php
+
+namespace Drupal\views_bulk_operations;
+
+use Drupal\Core\Url;
+
+/**
+ * Defines module Batch API methods.
+ */
+class ViewsBulkOperationsBatch {
+
+  /**
+   * Translation function wrapper.
+   *
+   * @see \Drupal\Core\StringTranslation\TranslationInterface:translate()
+   */
+  public static function t($string, array $args = [], array $options = []) {
+    return \Drupal::translation()->translate($string, $args, $options);
+  }
+
+  /**
+   * Set message function wrapper.
+   *
+   * @see \drupal_set_message()
+   */
+  public static function message($message = NULL, $type = 'status', $repeat = TRUE) {
+    drupal_set_message($message, $type, $repeat);
+  }
+
+  /**
+   * Gets the list of entities to process.
+   *
+   * Used in "all results" batch operation.
+   *
+   * @param array $data
+   *   Processed view data.
+   * @param array $context
+   *   Batch context.
+   */
+  public static function getList(array $data, array &$context) {
+    // Initialize batch.
+    if (empty($context['sandbox'])) {
+      $context['sandbox']['processed'] = 0;
+      $context['sandbox']['page'] = 0;
+      $context['results'] = $data;
+    }
+
+    $actionProcessor = \Drupal::service('views_bulk_operations.processor');
+    $actionProcessor->initialize($data);
+
+    // Populate queue.
+    $list = $actionProcessor->getPageList($context['sandbox']['page']);
+    $count = count($list);
+
+    if ($count) {
+      foreach ($list as $item) {
+        $context['results']['list'][] = $item;
+      }
+
+      $context['sandbox']['page']++;
+      $context['sandbox']['processed'] += $count;
+
+      // There may be cases where we don't know the total number of
+      // results (e.g. mini pager with a search_api view)
+      $context['finished'] = 0;
+      if ($data['total_results']) {
+        $context['finished'] = $context['sandbox']['processed'] / $data['total_results'];
+        $context['message'] = static::t('Prepared @count of @total entities for processing.', [
+          '@count' => $context['sandbox']['processed'],
+          '@total' => $data['total_results'],
+        ]);
+      }
+      else {
+        $context['message'] = static::t('Prepared @count entities for processing.', [
+          '@count' => $context['sandbox']['processed'],
+        ]);
+      }
+    }
+
+  }
+
+  /**
+   * Save generated list to user tempstore.
+   *
+   * @param bool $success
+   *   Was the process successfull?
+   * @param array $results
+   *   Batch process results array.
+   * @param array $operations
+   *   Performed operations array.
+   */
+  public static function saveList($success, array $results, array $operations) {
+    if ($success) {
+      $results['redirect_url'] = $results['redirect_after_processing'];
+      unset($results['redirect_after_processing']);
+      $tempstore_factory = \Drupal::service('user.private_tempstore');
+      $current_user = \Drupal::service('current_user');
+      $tempstore_name = 'views_bulk_operations_' . $results['view_id'] . '_' . $results['display_id'];
+      $results['prepopulated'] = TRUE;
+      $tempstore_factory->get($tempstore_name)->set($current_user->id(), $results);
+    }
+  }
+
+  /**
+   * Batch operation callback.
+   *
+   * @param array $data
+   *   Processed view data.
+   * @param array $context
+   *   Batch context.
+   */
+  public static function operation(array $data, array &$context) {
+    // Initialize batch.
+    if (empty($context['sandbox'])) {
+      $context['sandbox']['processed'] = 0;
+      $context['results']['operations'] = [];
+    }
+
+    // Get entities to process.
+    $actionProcessor = \Drupal::service('views_bulk_operations.processor');
+    $actionProcessor->initialize($data);
+
+    // Do the processing.
+    $count = $actionProcessor->populateQueue($data['list'], $context);
+    if ($count) {
+      $batch_results = $actionProcessor->process();
+      if (!empty($batch_results)) {
+        // Convert translatable markup to strings in order to allow
+        // correct operation of array_count_values function.
+        foreach ($batch_results as $result) {
+          $context['results']['operations'][] = (string) $result;
+        }
+      }
+      $context['sandbox']['processed'] += $count;
+
+      $context['finished'] = 0;
+      // There may be cases where we don't know the total number of
+      // results (probably all of them were already eliminated but
+      // leaving this code just in case).
+      if ($context['sandbox']['total']) {
+        $context['finished'] = $context['sandbox']['processed'] / $context['sandbox']['total'];
+        $context['message'] = static::t('Processed @count of @total entities.', [
+          '@count' => $context['sandbox']['processed'],
+          '@total' => $context['sandbox']['total'],
+        ]);
+      }
+      else {
+        $context['message'] = static::t('Processed @count entities.', [
+          '@count' => $context['sandbox']['processed'],
+        ]);
+      }
+    }
+  }
+
+  /**
+   * Batch finished callback.
+   *
+   * @param bool $success
+   *   Was the process successfull?
+   * @param array $results
+   *   Batch process results array.
+   * @param array $operations
+   *   Performed operations array.
+   */
+  public static function finished($success, array $results, array $operations) {
+    if ($success) {
+      $operations = array_count_values($results['operations']);
+      $details = [];
+      foreach ($operations as $op => $count) {
+        $details[] = $op . ' (' . $count . ')';
+      }
+      $message = static::t('Action processing results: @operations.', [
+        '@operations' => implode(', ', $details),
+      ]);
+      static::message($message);
+    }
+    else {
+      $message = static::t('Finished with an error.');
+      static::message($message, 'error');
+    }
+  }
+
+  /**
+   * Batch builder function.
+   *
+   * @param array $view_data
+   *   Processed view data.
+   */
+  public static function getBatch(array &$view_data) {
+    $current_class = get_called_class();
+
+    // Prepopulate results.
+    if (empty($view_data['list'])) {
+      // Redirect this batch to the processing URL and set
+      // previous redirect under a different key for later use.
+      $view_data['redirect_after_processing'] = $view_data['redirect_url'];
+      $view_data['redirect_url'] = Url::fromRoute('views_bulk_operations.execute_batch', [
+        'view_id' => $view_data['view_id'],
+        'display_id' => $view_data['display_id'],
+      ]);
+
+      $batch = [
+        'title' => static::t('Prepopulating entity list for processing.'),
+        'operations' => [
+          [
+            [$current_class, 'getList'],
+            [$view_data],
+          ],
+        ],
+        'progress_message' => static::t('Prepopulating, estimated time left: @estimate, elapsed: @elapsed.'),
+        'finished' => [$current_class, 'saveList'],
+      ];
+    }
+
+    // Execute action.
+    else {
+      $batch = [
+        'title' => static::t('Performing @operation on selected entities.', ['@operation' => $view_data['action_label']]),
+        'operations' => [
+          [
+            [$current_class, 'operation'],
+            [$view_data],
+          ],
+        ],
+        'progress_message' => static::t('Processing, estimated time left: @estimate, elapsed: @elapsed.'),
+        'finished' => [$current_class, 'finished'],
+      ];
+    }
+
+    return $batch;
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/src/ViewsBulkOperationsEvent.php b/web/modules/views_bulk_operations/src/ViewsBulkOperationsEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..44ad3e522c685f59211d6c947b09e57f5b768e88
--- /dev/null
+++ b/web/modules/views_bulk_operations/src/ViewsBulkOperationsEvent.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\views_bulk_operations;
+
+use Symfony\Component\EventDispatcher\Event;
+use Drupal\views\ViewExecutable;
+
+/**
+ * Defines Views Bulk Operations event type.
+ */
+class ViewsBulkOperationsEvent extends Event {
+
+  const NAME = 'views_bulk_operations.view_data';
+
+  /**
+   * The provider of the current view.
+   *
+   * @var string
+   */
+  protected $provider;
+
+  /**
+   * The views data of the current view.
+   *
+   * @var array
+   */
+  protected $viewData;
+
+  /**
+   * The current view object.
+   *
+   * @var \Drupal\views\ViewExecutable
+   */
+  protected $view;
+
+  /**
+   * IDs of entity types returned by the view.
+   *
+   * @var array
+   */
+  protected $entityTypeIds;
+
+  /**
+   * Row entity getter information.
+   *
+   * @var array
+   */
+  protected $entityGetter;
+
+  /**
+   * Object constructor.
+   *
+   * @param string $provider
+   *   The provider of the current view.
+   * @param array $viewData
+   *   The views data of the current view.
+   * @param \Drupal\views\ViewExecutable $view
+   *   The current view.
+   */
+  public function __construct($provider, array $viewData, ViewExecutable $view) {
+    $this->provider = $provider;
+    $this->viewData = $viewData;
+    $this->view = $view;
+  }
+
+  /**
+   * Get view provider.
+   *
+   * @return string
+   *   The view provider
+   */
+  public function getProvider() {
+    return $this->provider;
+  }
+
+  /**
+   * Get view data.
+   *
+   * @return string
+   *   The current view data
+   */
+  public function getViewData() {
+    return $this->viewData;
+  }
+
+  /**
+   * Get current view.
+   *
+   * @return \Drupal\views\ViewExecutable
+   *   The current view object
+   */
+  public function getView() {
+    return $this->view;
+  }
+
+  /**
+   * Get entity type IDs displayed by the current view.
+   *
+   * @return array
+   *   Entity type IDs.
+   */
+  public function getEntityTypeIds() {
+    return $this->entityTypeIds;
+  }
+
+  /**
+   * Get entity getter callable.
+   *
+   * @return array
+   *   Entity getter information.
+   */
+  public function getEntityGetter() {
+    return $this->entityGetter;
+  }
+
+  /**
+   * Set entity type IDs.
+   *
+   * @param array $entityTypeIds
+   *   Entity type IDs.
+   */
+  public function setEntityTypeIds(array $entityTypeIds) {
+    $this->entityTypeIds = $entityTypeIds;
+  }
+
+  /**
+   * Set entity getter callable.
+   *
+   * @param array $entityGetter
+   *   Entity getter information.
+   */
+  public function setEntityGetter(array $entityGetter) {
+    if (!isset($entityGetter['callable'])) {
+      throw new \Exception('Views Bulk Operations entity getter callable is not defined.');
+    }
+    $this->entityGetter = $entityGetter;
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/src/Functional/ViewsBulkOperationsBulkFormTest.php b/web/modules/views_bulk_operations/tests/src/Functional/ViewsBulkOperationsBulkFormTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..74c1170eb73f06fc10aea4bff4177371da20cb5f
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/src/Functional/ViewsBulkOperationsBulkFormTest.php
@@ -0,0 +1,344 @@
+<?php
+
+namespace Drupal\Tests\views_bulk_operations\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\views_bulk_operations\Plugin\views\field\ViewsBulkOperationsBulkForm
+ * @group views_bulk_operations
+ */
+class ViewsBulkOperationsBulkFormTest extends BrowserTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = [
+    'node',
+    'views',
+    'views_bulk_operations',
+    'views_bulk_operations_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create some nodes for testing.
+    $this->drupalCreateContentType(['type' => 'page']);
+
+    $this->testNodes = [];
+    $time = $this->container->get('datetime.time')->getRequestTime();
+    for ($i = 0; $i < 15; $i++) {
+      // Ensure nodes are sorted in the same order they are inserted in the
+      // array.
+      $time -= $i;
+      $this->testNodes[] = $this->drupalCreateNode([
+        'type' => 'page',
+        'title' => 'Title ' . $i,
+        'sticky' => FALSE,
+        'created' => $time,
+        'changed' => $time,
+      ]);
+    }
+
+  }
+
+  /**
+   * Helper function to test a batch process.
+   *
+   * After checking if we're on a Batch API page,
+   * the iterations are executed, the finished page is opened
+   * and browser redirects to the final destination.
+   *
+   * NOTE: As of Drupal 8.4, functional test
+   * automatically redirects user through all Batch API pages,
+   * so this function is not longer needed.
+   */
+  protected function assertBatchProcess() {
+    // Get the current batch ID.
+    $current_url = $this->getUrl();
+    $q = substr($current_url, strrpos($current_url, '/') + 1);
+    $this->assertEquals('batch?', substr($q, 0, 6), 'We are on a Batch API page.');
+
+    preg_match('#id=([0-9]+)#', $q, $matches);
+    $batch_id = $matches[1];
+
+    // Proceed with the operations.
+    // Assumption: all operations will be completed within a single request.
+    // TODO: modify code to include an option when the assumption is false.
+    do {
+      $this->drupalGet('batch', [
+        'query' => [
+          'id' => $batch_id,
+          'op' => 'do_nojs',
+        ],
+      ]);
+    } while (FALSE);
+
+    // Get the finished page.
+    $this->drupalGet('batch', [
+      'query' => [
+        'id' => $batch_id,
+        'op' => 'finished',
+      ],
+    ]);
+  }
+
+  /**
+   * Tests the VBO bulk form with simple test action.
+   */
+  public function testViewsBulkOperationsBulkFormSimple() {
+
+    $assertSession = $this->assertSession();
+
+    $this->drupalGet('views-bulk-operations-test');
+
+    // Test that the views edit header appears first.
+    $first_form_element = $this->xpath('//form/div[1][@id = :id]', [':id' => 'edit-header']);
+    $this->assertTrue($first_form_element, 'The views form edit header appears first.');
+
+    // Make sure a checkbox appears on all rows.
+    $edit = [];
+    for ($i = 0; $i < 4; $i++) {
+      $assertSession->fieldExists('edit-views-bulk-operations-bulk-form-' . $i, NULL, format_string('The checkbox on row @row appears.', ['@row' => $i]));
+    }
+
+    // The advanced action should not be shown on the form - no permission.
+    $this->assertTrue(empty($this->cssSelect('select[name=views_bulk_operations_advanced_test_action]')), t('Advanced action is not selectable.'));
+
+    // Log in as a user with 'edit any page content' permission
+    // to have access to perform the test operation.
+    $admin_user = $this->drupalCreateUser(['edit any page content']);
+    $this->drupalLogin($admin_user);
+
+    // Execute the simple test action.
+    $edit = [];
+    $selected = [0, 2, 3];
+    foreach ($selected as $index) {
+      $edit["views_bulk_operations_bulk_form[$index]"] = TRUE;
+    }
+
+    // Tests: actions as buttons, label override.
+    $this->drupalPostForm('views-bulk-operations-test', $edit, t('Simple test action'));
+
+    $testViewConfig = \Drupal::service('config.factory')->get('views.view.views_bulk_operations_test');
+    $configData = $testViewConfig->getRawData();
+    $preconfig_setting = $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['preconfiguration']['views_bulk_operations_simple_test_action']['preconfig'];
+
+    foreach ($selected as $index) {
+      $assertSession->pageTextContains(
+        sprintf('Test action (preconfig: %s, label: %s)',
+          $preconfig_setting,
+          $this->testNodes[$index]->label()
+        ),
+        sprintf('Action has been executed on node "%s".',
+          $this->testNodes[$index]->label()
+        )
+      );
+    }
+
+    // Test the select all functionality.
+    $edit = [
+      'select_all' => 1,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Simple test action'));
+
+    $assertSession->pageTextContains(
+      sprintf('Action processing results: Test (%d).', count($this->testNodes)),
+      sprintf('Action has been executed on %d nodes.', count($this->testNodes))
+    );
+
+  }
+
+  /**
+   * More advanced test.
+   *
+   * Uses the ViewsBulkOperationsAdvancedTestAction.
+   */
+  public function testViewsBulkOperationsBulkFormAdvanced() {
+
+    $assertSession = $this->assertSession();
+
+    // Log in as a user with 'edit any page content' permission
+    // to have access to perform the test operation.
+    $admin_user = $this->drupalCreateUser(['edit any page content', 'execute advanced test action']);
+    $this->drupalLogin($admin_user);
+
+    // First execute the simple action to test
+    // the ViewsBulkOperationsController class.
+    $edit = [
+      'action' => 'views_bulk_operations_simple_test_action',
+    ];
+    $selected = [0, 2];
+    foreach ($selected as $index) {
+      $edit["views_bulk_operations_bulk_form[$index]"] = TRUE;
+    }
+    $this->drupalPostForm('views-bulk-operations-test-advanced', $edit, t('Apply to selected items'));
+
+    $assertSession->pageTextContains(
+      sprintf('Action processing results: Test (%d).', count($selected)),
+      sprintf('Action has been executed on %d nodes.', count($selected))
+    );
+
+    // Execute the advanced test action.
+    $edit = [
+      'action' => 'views_bulk_operations_advanced_test_action',
+    ];
+    $selected = [0, 1, 3];
+    foreach ($selected as $index) {
+      $edit["views_bulk_operations_bulk_form[$index]"] = TRUE;
+    }
+    $this->drupalPostForm('views-bulk-operations-test-advanced', $edit, t('Apply to selected items'));
+
+    // Check if the configuration form is open and contains the
+    // test_config field.
+    $assertSession->fieldExists('edit-test-config', NULL, 'The configuration field appears.');
+
+    // Check if the configuration form contains selected entity labels.
+    // NOTE: The view pager has an offset set on this view, so checkbox
+    // indexes are not equal to test nodes array keys. Hence the $index + 1.
+    foreach ($selected as $index) {
+      $assertSession->pageTextContains($this->testNodes[$index + 1]->label());
+    }
+
+    $config_value = 'test value';
+    $edit = [
+      'test_config' => $config_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Apply'));
+
+    // Execute action by posting the confirmation form
+    // (also tests if the submit button exists on the page).
+    $this->drupalPostForm(NULL, [], t('Execute action'));
+
+    // If all went well and Batch API did its job,
+    // the next page should display results.
+    $testViewConfig = \Drupal::service('config.factory')->get('views.view.views_bulk_operations_test_advanced');
+    $configData = $testViewConfig->getRawData();
+    $preconfig_setting = $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['preconfiguration']['views_bulk_operations_advanced_test_action']['test_preconfig'];
+
+    // NOTE: The view pager has an offset set on this view, so checkbox
+    // indexes are not equal to test nodes array keys. Hence the $index + 1.
+    foreach ($selected as $index) {
+      $assertSession->pageTextContains(sprintf('Test action (preconfig: %s, config: %s, label: %s)',
+        $preconfig_setting,
+        $config_value,
+        $this->testNodes[$index + 1]->label()
+      ));
+    }
+
+    // Test the select all functionality with batching and entity
+    // property changes affecting view query results.
+    $edit = [
+      'action' => 'views_bulk_operations_advanced_test_action',
+      'select_all' => 1,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
+    $this->drupalPostForm(NULL, ['test_config' => 'unpublish'], t('Apply'));
+    $this->drupalPostForm(NULL, [], t('Execute action'));
+    // Again, take offset into account (-1).
+    $assertSession->pageTextContains(
+      sprintf('Action processing results: Test (%d).', (count($this->testNodes) - 1)),
+      sprintf('Action has been executed on all %d nodes.', (count($this->testNodes) - 1))
+    );
+    $this->assertTrue(empty($this->cssSelect('table.views-table tr')), t("The view doesn't show any results."));
+  }
+
+  /**
+   * View and context passing test.
+   *
+   * Uses the ViewsBulkOperationsPassTestAction.
+   */
+  public function testViewsBulkOperationsBulkFormPassing() {
+
+    $assertSession = $this->assertSession();
+
+    // Log in as a user with 'administer content' permission
+    // to have access to perform the test operation.
+    $admin_user = $this->drupalCreateUser(['bypass node access']);
+    $this->drupalLogin($admin_user);
+
+    // Test with all selected and specific selection, with batch
+    // size greater than items per page and lower than items per page,
+    // using Batch API process and without it.
+    $cases = [
+      ['batch' => FALSE, 'selection' => TRUE, 'page' => 1],
+      ['batch' => FALSE, 'selection' => FALSE],
+      ['batch' => TRUE, 'batch_size' => 3, 'selection' => TRUE, 'page' => 1],
+      ['batch' => TRUE, 'batch_size' => 7, 'selection' => TRUE],
+      ['batch' => TRUE, 'batch_size' => 3, 'selection' => FALSE],
+      ['batch' => TRUE, 'batch_size' => 7, 'selection' => FALSE],
+    ];
+
+    // Custom selection.
+    $selected = [0, 1, 3, 4];
+
+    $testViewConfig = \Drupal::service('config.factory')->getEditable('views.view.views_bulk_operations_test_advanced');
+    $configData = $testViewConfig->getRawData();
+    $configData['display']['default']['display_options']['pager']['options']['items_per_page'] = 5;
+
+    foreach ($cases as $case) {
+
+      // Populate form values.
+      $edit = [
+        'action' => 'views_bulk_operations_passing_test_action',
+      ];
+      if ($case['selection']) {
+        foreach ($selected as $index) {
+          $edit["views_bulk_operations_bulk_form[$index]"] = TRUE;
+        }
+      }
+      else {
+        $edit['select_all'] = 1;
+      }
+
+      // Update test view configuration.
+      $configData['display']['default']['display_options']['pager']['options']['items_per_page']++;
+      $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['batch'] = $case['batch'];
+      if (isset($case['batch_size'])) {
+        $configData['display']['default']['display_options']['fields']['views_bulk_operations_bulk_form']['batch_size'] = $case['batch_size'];
+      }
+      $testViewConfig->setData($configData);
+      $testViewConfig->save();
+
+      $options = [];
+      if (!empty($case['page'])) {
+        $options['query'] = ['page' => $case['page']];
+      }
+
+      $this->drupalGet('views-bulk-operations-test-advanced', $options);
+      $this->drupalPostForm(NULL, $edit, t('Apply to selected items'));
+
+      // On batch-enabled processes check if provided context data is correct.
+      if ($case['batch']) {
+        if ($case['selection']) {
+          $total = count($selected);
+        }
+        else {
+          // Again, include offset.
+          $total = count($this->testNodes) - 1;
+        }
+        $n_batches = ceil($total / $case['batch_size']);
+
+        for ($i = 0; $i < $n_batches; $i++) {
+          $processed = $i * $case['batch_size'];
+          $assertSession->pageTextContains(sprintf(
+            'Processed %s of %s.',
+            $processed,
+            $total
+          ), 'The correct processed info message appears.');
+        }
+      }
+
+      // Passed view integrity check.
+      $assertSession->pageTextContains('Passed view results match the entity queue.');
+    }
+
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsActionProcessorTest.php b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsActionProcessorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a62a0623354de1e2b7a70ff339cd92e8a48a62ed
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsActionProcessorTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\Tests\views_bulk_operations\Kernel;
+
+use Drupal\node\NodeInterface;
+
+/**
+ * @coversDefaultClass \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessor
+ * @group views_bulk_operations
+ */
+class ViewsBulkOperationsActionProcessorTest extends ViewsBulkOperationsKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->createTestNodes([
+      'page' => [
+        'count' => 20,
+      ],
+    ]);
+  }
+
+  /**
+   * Tests general functionality of ViewsBulkOperationsActionProcessor.
+   *
+   * @covers ::getPageList
+   * @covers ::populateQueue
+   * @covers ::process
+   */
+  public function testViewsbulkOperationsActionProcessor() {
+    $vbo_data = [
+      'view_id' => 'views_bulk_operations_test',
+      'action_id' => 'views_bulk_operations_simple_test_action',
+      'configuration' => [
+        'preconfig' => 'test',
+      ],
+    ];
+
+    // Test executing all view results first.
+    $results = $this->executeAction($vbo_data);
+
+    // The default batch size is 10 and there are 20 result rows total
+    // (10 nodes, each having a translation), check messages:
+    $this->assertEquals('Processed 10 of 20 entities.', $results['messages'][0]);
+    $this->assertEquals('Processed 20 of 20 entities.', $results['messages'][1]);
+    $this->assertEquals(20, $results['operations']['Test']);
+
+    // For a more advanced test, check if randomly selected entities
+    // have been unpublished.
+    $vbo_data = [
+      'view_id' => 'views_bulk_operations_test',
+      'action_id' => 'views_bulk_operations_advanced_test_action',
+      'preconfiguration' => [
+        'test_preconfig' => 'test',
+        'test_config' => 'unpublish',
+      ],
+    ];
+
+    // Get list of rows to process from different view pages.
+    $selection = [0, 3, 6, 8, 15, 16, 18];
+    $vbo_data['list'] = $this->getResultsList($vbo_data, $selection);
+
+    // Execute the action.
+    $results = $this->executeAction($vbo_data);
+
+    $nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
+
+    $statuses = [];
+
+    foreach ($this->testNodesData as $id => $lang_data) {
+      $node = $nodeStorage->load($id);
+      $statuses[$id] = intval($node->status->value);
+    }
+
+    foreach ($statuses as $id => $status) {
+      foreach ($vbo_data['list'] as $item) {
+        if ($item[3] == $id) {
+          $this->assertEquals(NodeInterface::NOT_PUBLISHED, $status);
+          break 2;
+        }
+      }
+      $this->assertEquals(NodeInterface::PUBLISHED, $status);
+    }
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsDataServiceTest.php b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsDataServiceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a004a32b1d45d9f162240ad1f8ee65e7498762f
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsDataServiceTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\Tests\views_bulk_operations\Kernel;
+
+use Drupal\views\Views;
+
+/**
+ * @coversDefaultClass \Drupal\views_bulk_operations\Service\ViewsBulkOperationsViewData
+ * @group views_bulk_operations
+ */
+class ViewsBulkOperationsDataServiceTest extends ViewsBulkOperationsKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->createTestNodes([
+      'page' => [
+        'languages' => ['pl', 'es', 'it', 'fr', 'de'],
+        'count' => 20,
+      ],
+    ]);
+  }
+
+  /**
+   * Tests the getEntityDefault() method.
+   *
+   * @covers ::getEntityDefault
+   */
+  public function testViewsbulkOperationsViewDataEntityGetter() {
+    // Initialize and execute the test view with all items displayed.
+    $view = Views::getView('views_bulk_operations_test');
+    $view->setDisplay('page_1');
+    $view->setItemsPerPage(0);
+    $view->setCurrentPage(0);
+    $view->execute();
+
+    $test_data = $this->testNodesData;
+    foreach ($view->result as $row) {
+      $entity = $this->vboDataService->getEntityDefault($row, 'none', $view);
+
+      $expected_label = $test_data[$entity->id()][$entity->language()->getId()];
+
+      $this->assertEquals($expected_label, $entity->label(), 'Title matches');
+      if ($expected_label === $entity->label()) {
+        unset($test_data[$entity->id()][$entity->language()->getId()]);
+        if (empty($test_data[$entity->id()])) {
+          unset($test_data[$entity->id()]);
+        }
+      }
+    }
+
+    $this->assertEmpty($test_data, 'All created entities and their translations were returned.');
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsKernelTestBase.php b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsKernelTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..8dcf355154874f29739de8e6d3f7ba13b661f784
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/src/Kernel/ViewsBulkOperationsKernelTestBase.php
@@ -0,0 +1,297 @@
+<?php
+
+namespace Drupal\Tests\views_bulk_operations\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\simpletest\NodeCreationTrait;
+use Drupal\node\Entity\NodeType;
+use Drupal\user\Entity\User;
+use Drupal\views\Views;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\views_bulk_operations\ViewsBulkOperationsBatch;
+
+/**
+ * Base class for Views Bulk Operations kernel tests.
+ */
+abstract class ViewsBulkOperationsKernelTestBase extends KernelTestBase {
+
+  use NodeCreationTrait {
+    getNodeByTitle as drupalGetNodeByTitle;
+    createNode as drupalCreateNode;
+  }
+
+  // To be removed.
+  const TEST_NODES_COUNT = 10;
+
+  const VBO_DEFAULTS = [
+    'list' => [],
+    'display_id' => 'default',
+    'preconfiguration' => [],
+    'batch' => TRUE,
+    'arguments' => [],
+    'exposed_input' => [],
+    'batch_size' => 10,
+    'relationship_id' => 'none',
+  ];
+
+  /**
+   * Test node types already created.
+   *
+   * @var array
+   */
+  protected $testNodesTypes;
+
+
+  /**
+   * Test nodes data including titles and languages.
+   *
+   * @var array
+   */
+  protected $testNodesData;
+
+  /**
+   * VBO views data service.
+   *
+   * @var \Drupal\views_bulk_operations\Service\ViewsBulkOperationsViewDataInterface
+   */
+  protected $vboDataService;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'user',
+    'node',
+    'field',
+    'content_translation',
+    'views_bulk_operations',
+    'views_bulk_operations_test',
+    'views',
+    'filter',
+    'language',
+    'text',
+    'action',
+    'system',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('node');
+    $this->installSchema('node', 'node_access');
+    $this->installSchema('system', 'sequences');
+    $this->installSchema('system', 'key_value_expire');
+
+    $user = User::create();
+    $user->setPassword('password');
+    $user->enforceIsNew();
+    $user->setEmail('email');
+    $user->setUsername('user_name');
+    $user->save();
+    user_login_finalize($user);
+
+    $this->installConfig([
+      'system',
+      'filter',
+      'views_bulk_operations_test',
+      'language',
+    ]);
+
+    // Get time and VBO view data services.
+    $this->time = $this->container->get('datetime.time');
+    $this->vboDataService = $this->container->get('views_bulk_operations.data');
+  }
+
+  /**
+   * Create some test nodes.
+   *
+   * @param array $test_node_data
+   *   Describes test node bundles and properties.
+   *
+   * @see Drupal\Tests\views_bulk_operations\Kernel\ViewsBulkOperationsDataServiceTest::setUp()
+   */
+  protected function createTestNodes(array $test_node_data) {
+    $this->testNodesData = [];
+    foreach ($test_node_data as $type_name => $type_data) {
+      $type = NodeType::create([
+        'type' => $type_name,
+        'name' => $type_name,
+      ]);
+      $type->save();
+
+      $count_languages = isset($type_data['languages']) ? count($type_data['languages']) : 0;
+      if ($count_languages) {
+        for ($i = 0; $i < $count_languages; $i++) {
+          $language = ConfigurableLanguage::createFromLangcode($type_data['languages'][$i]);
+          $language->save();
+        }
+        $this->container->get('content_translation.manager')->setEnabled('node', $type_name, TRUE);
+        // $this->container->get('entity_type.manager')->clearCachedDefinitions();
+      }
+
+      // Create some test nodes.
+      $time = $this->time->getRequestTime();
+      if (!isset($type_data['count'])) {
+        $type_data['count'] = 10;
+      }
+      for ($i = 0; $i < $type_data['count']; $i++) {
+        $time -= $i;
+        $title = 'Title ' . $i;
+        $node = $this->drupalCreateNode([
+          'type' => $type_name,
+          'title' => $title,
+          'sticky' => FALSE,
+          'created' => $time,
+          'changed' => $time,
+        ]);
+        $this->testNodesData[$node->id()]['en'] = $title;
+
+        if ($count_languages) {
+          // It doesn't really matter to which languages we translate
+          // from the API point of view so some randomness should be fine.
+          $langcode = $type_data['languages'][rand(0, $count_languages - 1)];
+          $title = 'Translated title ' . $langcode . ' ' . $i;
+          $translation = $node->addTranslation($langcode, [
+            'title' => $title,
+          ]);
+          $translation->save();
+          $this->testNodesData[$node->id()][$langcode] = $title;
+        }
+      }
+    }
+  }
+
+  /**
+   * Initialize and return the view described by $vbo_data.
+   *
+   * @param array $vbo_data
+   *   An array of data passed to VBO Processor service.
+   *
+   * @return \Drupal\views\ViewExecutable
+   *   The view object.
+   */
+  protected function initializeView(array $vbo_data) {
+    if (!$view = Views::getView($vbo_data['view_id'])) {
+      throw new \Exception('Incorrect view ID provided.');
+    }
+    if (!$view->setDisplay($vbo_data['display_id'])) {
+      throw new \Exception('Incorrect view display ID provided.');
+    }
+    $view->built = FALSE;
+    $view->executed = FALSE;
+
+    return $view;
+  }
+
+  /**
+   * Get a random list of results bulk keys.
+   *
+   * @param array $vbo_data
+   *   An array of data passed to VBO Processor service.
+   * @param array $deltas
+   *   Array of result rows deltas.
+   *
+   * @return array
+   *   List of results to process.
+   */
+  protected function getResultsList(array $vbo_data, array $deltas) {
+    // Merge in defaults.
+    $vbo_data += static::VBO_DEFAULTS;
+
+    $view = $this->initializeView($vbo_data);
+    if (!empty($vbo_data['arguments'])) {
+      $view->setArguments($vbo_data['arguments']);
+    }
+    if (!empty($vbo_data['exposed_input'])) {
+      $view->setExposedInput($vbo_data['exposed_input']);
+    }
+
+    $view->setItemsPerPage(0);
+    $view->setCurrentPage(0);
+    $view->execute();
+
+    $this->vboDataService->init($view, $view->getDisplay(), $vbo_data['relationship_id']);
+
+    $list = [];
+    $base_field = $view->storage->get('base_field');
+    foreach ($deltas as $delta) {
+      $entity = $this->vboDataService->getEntity($view->result[$delta]);
+
+      $list[] = [
+        $view->result[$delta]->{$base_field},
+        $entity->language()->getId(),
+        $entity->getEntityTypeId(),
+        $entity->id(),
+      ];
+    }
+
+    $view->destroy();
+
+    return $list;
+  }
+
+  /**
+   * Execute an action on a specific view results.
+   *
+   * @param array $vbo_data
+   *   An array of data passed to VBO Processor service.
+   */
+  protected function executeAction(array $vbo_data) {
+
+    // Merge in defaults.
+    $vbo_data += static::VBO_DEFAULTS;
+
+    $view = $this->initializeView($vbo_data);
+    $view->get_total_rows = TRUE;
+
+    $view->execute();
+
+    // Get total rows count.
+    $this->vboDataService->init($view, $view->getDisplay(), $vbo_data['relationship_id']);
+    $vbo_data['total_results'] = $this->vboDataService->getTotalResults();
+
+    // Get action definition and check if action ID is correct.
+    $action_definition = $this->container->get('plugin.manager.views_bulk_operations_action')->getDefinition($vbo_data['action_id']);
+    if (!isset($vbo_data['action_label'])) {
+      $vbo_data['action_label'] = (string) $action_definition['label'];
+    }
+
+    // Populate entity list if empty.
+    if (empty($vbo_data['list'])) {
+      $context = [];
+      do {
+        $context['finished'] = 1;
+        $context['message'] = '';
+        ViewsBulkOperationsBatch::getList($vbo_data, $context);
+      } while ($context['finished'] < 1);
+      $vbo_data = $context['results'];
+    }
+
+    $summary = [
+      'messages' => [],
+    ];
+
+    // Execute the selected action.
+    $context = [];
+    do {
+      $context['finished'] = 1;
+      $context['message'] = '';
+      ViewsBulkOperationsBatch::operation($vbo_data, $context);
+      if (!empty($context['message'])) {
+        $summary['messages'][] = (string) $context['message'];
+      }
+    } while ($context['finished'] < 1);
+
+    // Add information to the summary array.
+    $summary += [
+      'operations' => array_count_values($context['results']['operations']),
+    ];
+
+    return $summary;
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/src/Unit/TestViewsBulkOperationsBatch.php b/web/modules/views_bulk_operations/tests/src/Unit/TestViewsBulkOperationsBatch.php
new file mode 100644
index 0000000000000000000000000000000000000000..cbf30569b30c26822ab91755c749dfc8cad6f599
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/src/Unit/TestViewsBulkOperationsBatch.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\Tests\views_bulk_operations\Unit;
+
+use Drupal\views_bulk_operations\ViewsBulkOperationsBatch;
+
+/**
+ * Override some class methods for proper testing.
+ */
+class TestViewsBulkOperationsBatch extends ViewsBulkOperationsBatch {
+
+  /**
+   * Override t method.
+   */
+  public static function t($string, array $args = [], array $options = []) {
+    return strtr($string, $args);
+  }
+
+  /**
+   * Override message method.
+   */
+  public static function message($message = NULL, $type = 'status', $repeat = TRUE) {
+    static $storage;
+    if (isset($storage)) {
+      $output = $storage;
+      $storage = NULL;
+      return $output;
+    }
+    else {
+      $storage = (string) $message;
+    }
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/src/Unit/ViewsBulkOperationsBatchTest.php b/web/modules/views_bulk_operations/tests/src/Unit/ViewsBulkOperationsBatchTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5c8f16eebe3d5f06866b59cc79a2fdab90a694d1
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/src/Unit/ViewsBulkOperationsBatchTest.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Drupal\Tests\views_bulk_operations\Unit;
+
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Drupal\views\Entity\View;
+
+/**
+ * @coversDefaultClass \Drupal\views_bulk_operations\ViewsBulkOperationsBatch
+ * @group views_bulk_operations
+ */
+class ViewsBulkOperationsBatchTest extends UnitTestCase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = ['node'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->container = new ContainerBuilder();
+    \Drupal::setContainer($this->container);
+  }
+
+  /**
+   * Returns a stub ViewsBulkOperationsActionProcessor that returns dummy data.
+   *
+   * @return \Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessor
+   *   A mocked action processor.
+   */
+  public function getViewsBulkOperationsActionProcessorStub($entities_count) {
+    $actionProcessor = $this->getMockBuilder('Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessor')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $actionProcessor->expects($this->any())
+      ->method('populateQueue')
+      ->will($this->returnValue($entities_count));
+
+    $actionProcessor->expects($this->any())
+      ->method('process')
+      ->will($this->returnCallback(function () use ($entities_count) {
+        $return = [];
+        for ($i = 0; $i < $entities_count; $i++) {
+          $return[] = 'Some action';
+        }
+        return $return;
+      }));
+
+    return $actionProcessor;
+  }
+
+  /**
+   * Tests the getBatch() method.
+   *
+   * @covers ::getBatch
+   */
+  public function testGetBatch() {
+    $data = [
+      'list' => [[0, 'en', 'node', 1]],
+      'some_data' => [],
+      'action_label' => '',
+    ];
+    $batch = TestViewsBulkOperationsBatch::getBatch($data);
+    $this->assertArrayHasKey('title', $batch);
+    $this->assertArrayHasKey('operations', $batch);
+    $this->assertArrayHasKey('finished', $batch);
+  }
+
+  /**
+   * Tests the operation() method.
+   *
+   * @covers ::operation
+   */
+  public function testOperation() {
+    $batch_size = 2;
+    $entities_count = 10;
+
+    $this->container->set('views_bulk_operations.processor', $this->getViewsBulkOperationsActionProcessorStub($batch_size));
+
+    $view = new View(['id' => 'test_view'], 'view');
+    $view_storage = $this->getMockBuilder('Drupal\Core\Config\Entity\ConfigEntityStorage')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $view_storage->expects($this->any())
+      ->method('load')
+      ->with('test_view')
+      ->will($this->returnValue($view));
+
+    $entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
+    $entity_manager->expects($this->any())
+      ->method('getStorage')
+      ->with('view')
+      ->will($this->returnValue($view_storage));
+    $this->container->set('entity.manager', $entity_manager);
+
+    $executable = $this->getMockBuilder('Drupal\views\ViewExecutable')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $executable->result = [];
+
+    // We set only $batch_size entities because
+    // $view->setItemsPerPage will not have effect.
+    for ($i = 0; $i < $batch_size; $i++) {
+      $row = new \stdClass();
+      $row->_entity = new \stdClass();
+      $executable->result[] = $row;
+    }
+
+    $viewExecutableFactory = $this->getMockBuilder('Drupal\views\ViewExecutableFactory')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $viewExecutableFactory->expects($this->any())
+      ->method('get')
+      ->will($this->returnValue($executable));
+    $this->container->set('views.executable', $viewExecutableFactory);
+
+    $data = [
+      'view_id' => 'test_view',
+      'display_id' => 'test_display',
+      'batch_size' => $batch_size,
+      'list' => [],
+    ];
+    $context = [
+      'sandbox' => [
+        'processed' => 0,
+        'total' => $entities_count,
+      ],
+    ];
+
+    TestViewsBulkOperationsBatch::operation($data, $context);
+
+    $this->assertEquals(count($context['results']['operations']), $batch_size);
+    $this->assertEquals($context['finished'], ($batch_size / $entities_count));
+  }
+
+  /**
+   * Tests the finished() method.
+   *
+   * @covers ::finished
+   */
+  public function testFinished() {
+    $results = ['operations' => ['Some operation', 'Some operation']];
+    TestViewsBulkOperationsBatch::finished(TRUE, $results, []);
+    $this->assertEquals(TestViewsBulkOperationsBatch::message(), 'Action processing results: Some operation (2).');
+
+    $results = ['operations' => ['Some operation1', 'Some operation2']];
+    TestViewsBulkOperationsBatch::finished(TRUE, $results, []);
+    $this->assertEquals(TestViewsBulkOperationsBatch::message(), 'Action processing results: Some operation1 (1), Some operation2 (1).');
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test.yml b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9a2d0057ff8432655c8d677883ca9849a2abefa1
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test.yml
@@ -0,0 +1,232 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+    - user
+    - views_bulk_operations
+id: views_bulk_operations_test
+label: 'Views Bulk Operations Test'
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      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
+      pager:
+        type: full
+        options:
+          items_per_page: 4
+          offset: 0
+          id: 0
+          total_pages: null
+          tags:
+            previous: ‹‹
+            next: ››
+            first: '« First'
+            last: 'Last »'
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          quantity: 9
+      style:
+        type: table
+      row:
+        type: fields
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          entity_type: node
+          entity_field: title
+          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
+          settings:
+            link_to_entity: true
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          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
+          click_sort_column: value
+          type: string
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        views_bulk_operations_bulk_form:
+          id: views_bulk_operations_bulk_form
+          table: views
+          field: views_bulk_operations_bulk_form
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Views bulk operations'
+          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: 0
+            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
+          batch: false
+          batch_size: 10
+          form_step: true
+          buttons: true
+          action_title: Action
+          selected_actions:
+            views_bulk_operations_simple_test_action: views_bulk_operations_simple_test_action
+            views_bulk_operations_advanced_test_action: views_bulk_operations_advanced_test_action
+          preconfiguration:
+            views_bulk_operations_simple_test_action:
+              label_override: 'Simple test action'
+              preconfig: 'Test setting'
+            views_bulk_operations_advanced_test_action:
+              label_override: ''
+              preconfig: 'Test setting'
+          plugin_id: views_bulk_operations_bulk_form
+      filters: {  }
+      sorts:
+        created:
+          id: created
+          table: node_field_data
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: DESC
+          exposed: false
+          expose:
+            label: ''
+          granularity: second
+          entity_type: node
+          entity_field: created
+          plugin_id: date
+      title: 'Views Bulk Operations Test'
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: views-bulk-operations-test
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test_advanced.yml b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test_advanced.yml
new file mode 100644
index 0000000000000000000000000000000000000000..67805a381b99262d1edd2784cc3673ac7a14b74d
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/config/install/views.view.views_bulk_operations_test_advanced.yml
@@ -0,0 +1,270 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - node
+    - user
+    - views_bulk_operations
+id: views_bulk_operations_test_advanced
+label: 'Views Bulk Operations Advanced Test'
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+      cache:
+        type: none
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      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
+      pager:
+        type: mini
+        options:
+          items_per_page: 4
+          offset: 1
+          id: 0
+          total_pages: null
+          tags:
+            previous: ‹‹
+            next: ››
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      style:
+        type: table
+      row:
+        type: fields
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          entity_type: node
+          entity_field: title
+          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
+          settings:
+            link_to_entity: true
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          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
+          click_sort_column: value
+          type: string
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        views_bulk_operations_bulk_form:
+          id: views_bulk_operations_bulk_form
+          table: views
+          field: views_bulk_operations_bulk_form
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Views bulk operations'
+          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: 0
+            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
+          batch: true
+          batch_size: 2
+          form_step: true
+          buttons: false
+          action_title: Action
+          selected_actions:
+            views_bulk_operations_simple_test_action: views_bulk_operations_simple_test_action
+            views_bulk_operations_advanced_test_action: views_bulk_operations_advanced_test_action
+            views_bulk_operations_passing_test_action: views_bulk_operations_passing_test_action
+          preconfiguration:
+            views_bulk_operations_simple_test_action:
+              label_override: 'Simple test action'
+              preconfig: 'Test setting'
+            views_bulk_operations_advanced_test_action:
+              label_override: ''
+              test_preconfig: 'Test setting'
+            views_bulk_operations_passing_test_action:
+              label_override: ''
+          plugin_id: views_bulk_operations_bulk_form
+      filters:
+        status:
+          id: status
+          table: node_field_data
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          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: {  }
+          entity_type: node
+          entity_field: status
+          plugin_id: boolean
+      sorts:
+        created:
+          id: created
+          table: node_field_data
+          field: created
+          relationship: none
+          group_type: group
+          admin_label: ''
+          order: DESC
+          exposed: false
+          expose:
+            label: ''
+          granularity: second
+          entity_type: node
+          entity_field: created
+          plugin_id: date
+      title: 'Views Bulk Operations Test'
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: views-bulk-operations-test-advanced
+    cache_metadata:
+      max-age: 0
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
+        - 'user.node_grants:view'
+        - user.permissions
+      tags: {  }
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsAdvancedTestAction.php b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsAdvancedTestAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..b5670268800a868498dce15e776ec04153a09e81
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsAdvancedTestAction.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Drupal\views_bulk_operations_test\Plugin\Action;
+
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsPreconfigurationInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\views\ViewExecutable;
+
+/**
+ * Action for test purposes only.
+ *
+ * @Action(
+ *   id = "views_bulk_operations_advanced_test_action",
+ *   label = @Translation("VBO advanced test action"),
+ *   type = "",
+ *   confirm = TRUE,
+ *   requirements = {
+ *     "_permission" = "execute advanced test action",
+ *   },
+ * )
+ */
+class ViewsBulkOperationsAdvancedTestAction extends ViewsBulkOperationsActionBase implements ViewsBulkOperationsPreconfigurationInterface, PluginFormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    // Check if $this->view is an instance of ViewsExecutable.
+    if (!($this->view instanceof ViewExecutable)) {
+      throw new \Exception('View passed to action object is not an instance of \Drupal\views\ViewExecutable.');
+    }
+
+    // Check if context array has been passed to the action.
+    if (empty($this->context)) {
+      throw new \Exception('Context array empty in action object.');
+    }
+
+    drupal_set_message(sprintf('Test action (preconfig: %s, config: %s, label: %s)',
+      $this->configuration['test_preconfig'],
+      $this->configuration['test_config'],
+      $entity->label()
+    ));
+
+    // Unpublish entity.
+    if ($this->configuration['test_config'] === 'unpublish') {
+      if (!$entity->isDefaultTranslation()) {
+        $entity = \Drupal::service('entity_type.manager')->getStorage('node')->load($entity->id());
+      }
+      $entity->setPublished(FALSE);
+      $entity->save();
+    }
+
+    return 'Test';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildPreConfigurationForm(array $element, array $values, FormStateInterface $form_state) {
+    $element['test_preconfig'] = [
+      '#title' => $this->t('Preliminary configuration'),
+      '#type' => 'textfield',
+      '#default_value' => isset($values['preconfig']) ? $values['preconfig'] : '',
+    ];
+    return $element;
+  }
+
+  /**
+   * Configuration form builder.
+   *
+   * @param array $form
+   *   Form array.
+   * @param Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
+   *
+   * @return array
+   *   The configuration form.
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['test_config'] = [
+      '#title' => t('Config'),
+      '#type' => 'textfield',
+      '#default_value' => $form_state->getValue('config'),
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    return $object->access('update', $account, $return_as_object);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsPassTestAction.php b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsPassTestAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2c0eecb5a013c3e928dd7581d4a15a2ebf07f8b
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsPassTestAction.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\views_bulk_operations_test\Plugin\Action;
+
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Action for test purposes only.
+ *
+ * @Action(
+ *   id = "views_bulk_operations_passing_test_action",
+ *   label = @Translation("VBO parameters passing test action"),
+ *   type = "node",
+ * )
+ */
+class ViewsBulkOperationsPassTestAction extends ViewsBulkOperationsActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeMultiple(array $nodes) {
+    if (!empty($this->context['sandbox'])) {
+      drupal_set_message(sprintf(
+        'Processed %s of %s.',
+        $this->context['sandbox']['processed'],
+        $this->context['sandbox']['total']
+      ));
+    }
+
+    // Check if the passed view result rows contain the correct nodes.
+    if (empty($this->context['sandbox']['result_pass_error'])) {
+      $this->view->result = array_values($this->view->result);
+      foreach ($nodes as $index => $node) {
+        $result_node = $this->view->result[$index]->_entity;
+        if (
+          $node->id() !== $result_node->id() ||
+          $node->label() !== $result_node->label()
+        ) {
+          $this->context['sandbox']['result_pass_error'] = TRUE;
+        }
+      }
+    }
+
+    $batch_size = isset($this->context['sandbox']['batch_size']) ? $this->context['sandbox']['batch_size'] : 0;
+    $total = isset($this->context['sandbox']['total']) ? $this->context['sandbox']['total'] : 0;
+    $processed = isset($this->context['sandbox']['processed']) ? $this->context['sandbox']['processed'] : 0;
+
+    // On last batch display message if passed rows match.
+    if ($processed + $batch_size >= $total) {
+      if (empty($this->context['sandbox']['result_pass_error'])) {
+        drupal_set_message('Passed view results match the entity queue.');
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    $this->executeMultiple([$entity]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    return $object->access('update', $account, $return_as_object);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsSimpleTestAction.php b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsSimpleTestAction.php
new file mode 100644
index 0000000000000000000000000000000000000000..a8e0d33886da442666ee29ef1e858a5ce804504a
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/src/Plugin/Action/ViewsBulkOperationsSimpleTestAction.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\views_bulk_operations_test\Plugin\Action;
+
+use Drupal\views_bulk_operations\Action\ViewsBulkOperationsActionBase;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Action for test purposes only.
+ *
+ * @Action(
+ *   id = "views_bulk_operations_simple_test_action",
+ *   label = @Translation("VBO simple test action"),
+ *   type = "node"
+ * )
+ */
+class ViewsBulkOperationsSimpleTestAction extends ViewsBulkOperationsActionBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($entity = NULL) {
+    drupal_set_message(sprintf('Test action (preconfig: %s, label: %s)',
+      $this->configuration['preconfig'],
+      $entity->label()
+    ));
+    return 'Test';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
+    return $object->access('update', $account, $return_as_object);
+  }
+
+}
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.info.yml b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b990f79660fdb401782195cd497623e46f632567
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.info.yml
@@ -0,0 +1,14 @@
+name: 'Views Bulk Operations test'
+type: module
+description: 'Support module for testing Views Bulk Operations.'
+package: Testing
+# core: 8.x
+dependencies:
+  - drupal:views_bulk_operations
+  - drupal:node
+
+# Information added by Drupal.org packaging script on 2018-07-02
+version: '8.x-2.4'
+core: '8.x'
+project: 'views_bulk_operations'
+datestamp: 1530516826
diff --git a/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.permissions.yml b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0fa2f9e7bef92577277c9660965100c9c68aeb7f
--- /dev/null
+++ b/web/modules/views_bulk_operations/tests/views_bulk_operations_test/views_bulk_operations_test.permissions.yml
@@ -0,0 +1,2 @@
+execute advanced test action:
+  title: 'Execute advanced test action'
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.drush.inc b/web/modules/views_bulk_operations/views_bulk_operations.drush.inc
new file mode 100644
index 0000000000000000000000000000000000000000..4593d13f58f4cf606e594752219be1e9e158d597
--- /dev/null
+++ b/web/modules/views_bulk_operations/views_bulk_operations.drush.inc
@@ -0,0 +1,207 @@
+<?php
+
+/**
+ * @file
+ * Contains code providing drush commands functionality.
+ */
+
+use Drupal\views\Views;
+use Drupal\views_bulk_operations\ViewsBulkOperationsBatch;
+
+/**
+ * Implements hook_drush_command().
+ */
+function views_bulk_operations_drush_command() {
+  return [
+    'views-bulk-operations-execute' => [
+      'description' => 'Execute an action on all results of the given view.',
+      'aliases' => ['vbo-execute', 'vbo-exec'],
+      'arguments' => [
+        'view_id' => 'The ID of the view to use',
+        'action_id' => 'The ID of the action to execute',
+      ],
+      'options' => [
+        'display-id' => 'ID of the display to use (default: default)',
+        'args' => 'View arguments (slash is a delimeter, default: none)',
+        'exposed' => 'Exposed filters (query string format)',
+        'batch-size' => 'Processing batch size (default: 100)',
+        'config' => 'Action configuration (query string format)',
+        'debug' => 'Include additional debugging information.',
+      ],
+      'examples' => [
+        'drush vbo-execute some_view some_action --user=1' => 'Execute some action on some view as the superuser.',
+        'drush vbo-execute some_view some_action --args=arg1/arg2 --batch-size=50' => 'Execute some action on some view with arg1 and arg2 as view arguments and 50 entities processed per batch.',
+        'drush vbo-execute some_view some_action --config="key1=value1&key2=value2"' => 'Execute some action on some view with action configuration set.',
+      ],
+    ],
+  ];
+}
+
+/**
+ * Helper function to set / get timer.
+ *
+ * @param bool $debug
+ *   Should the function do anything at all?
+ * @param string $id
+ *   ID of a specific timer span.
+ *
+ * @return mixed
+ *   NULL or value of a specific timer if set.
+ */
+function _views_bulk_operations_timer($debug = TRUE, $id = NULL) {
+  if (!$debug) {
+    return;
+  }
+
+  static $timers = [];
+
+  if (!isset($id)) {
+    $timers['start'] = microtime(TRUE);
+  }
+  else {
+    if (isset($timers[$id])) {
+      end($timers);
+      do {
+        if (key($timers) === $id) {
+          return round((current($timers) - prev($timers)) * 1000, 3);
+        }
+        else {
+          $result = prev($timers);
+        }
+      } while ($result);
+    }
+    else {
+      $timers[$id] = microtime(TRUE);
+    }
+  }
+}
+
+/**
+ * The vbo-exec command executtion function.
+ *
+ * @param string $view_id
+ *   The ID of the view to use.
+ * @param string $action_id
+ *   The ID of the action to execute.
+ */
+function drush_views_bulk_operations_execute($view_id, $action_id) {
+
+  $debug = drush_get_option('debug', FALSE);
+  _views_bulk_operations_timer($debug);
+
+  // Prepare parameters.
+  $arguments = drush_get_option('args', FALSE);
+  if ($arguments) {
+    $arguments = explode('/', $arguments);
+  }
+
+  $qs_config = [
+    'config' => [],
+    'exposed' => [],
+  ];
+  foreach ($qs_config as $name => $value) {
+    $config_data = drush_get_option($name, []);
+    if (!empty($config_data)) {
+      parse_str($config_data, $qs_config[$name]);
+    }
+  }
+
+  $vbo_data = [
+    'list' => [],
+    'view_id' => $view_id,
+    'display_id' => drush_get_option('display-id', 'default'),
+    'action_id' => $action_id,
+    'preconfiguration' => $qs_config['config'],
+    'batch' => TRUE,
+    'arguments' => $arguments,
+    'exposed_input' => $qs_config['exposed'],
+    'batch_size' => drush_get_option('batch-size', 100),
+    'relationship_id' => 'none',
+  ];
+
+  // Initialize the view to check if parameters are correct.
+  if (!$view = Views::getView($vbo_data['view_id'])) {
+    drush_set_error('Incorrect view ID provided.');
+    return;
+  }
+  if (!$view->setDisplay($vbo_data['display_id'])) {
+    drush_set_error('Incorrect view display ID provided.');
+    return;
+  }
+  if (!empty($vbo_data['arguments'])) {
+    $view->setArguments($vbo_data['arguments']);
+  }
+  if (!empty($vbo_data['exposed_input'])) {
+    $view->setExposedInput($vbo_data['exposed_input']);
+  }
+
+  // We need total rows count for proper progress message display.
+  $view->get_total_rows = TRUE;
+  $view->execute();
+
+  // Get relationship ID if VBO field exists.
+  $vbo_data['relationship_id'] = 'none';
+  foreach ($view->field as $field) {
+    if ($field->options['id'] === 'views_bulk_operations_bulk_form') {
+      $vbo_data['relationship_id'] = $field->options['relationship'];
+    }
+  }
+
+  // Get total rows count.
+  $viewDataService = \Drupal::service('views_bulk_operations.data');
+  $viewDataService->init($view, $view->getDisplay(), $vbo_data['relationship_id']);
+  $vbo_data['total_results'] = $viewDataService->getTotalResults();
+
+  // Get action definition and check if action ID is correct.
+  try {
+    $action_definition = \Drupal::service('plugin.manager.views_bulk_operations_action')->getDefinition($action_id);
+  }
+  catch (\Exception $e) {
+    drush_set_error($e->getMessage());
+    return;
+  }
+  $vbo_data['action_label'] = (string) $action_definition['label'];
+
+  _views_bulk_operations_timer($debug, 'init');
+
+  // Populate entity list.
+  $context = [];
+  do {
+    $context['finished'] = 1;
+    $context['message'] = '';
+    ViewsBulkOperationsBatch::getList($vbo_data, $context);
+    if (!empty($context['message'])) {
+      drush_log($context['message'], 'ok');
+    }
+  } while ($context['finished'] < 1);
+  $vbo_data = $context['results'];
+
+  _views_bulk_operations_timer($debug, 'list');
+
+  // Execute the selected action.
+  $context = [];
+  do {
+    $context['finished'] = 1;
+    $context['message'] = '';
+    ViewsBulkOperationsBatch::operation($vbo_data, $context);
+    if (!empty($context['message'])) {
+      drush_log($context['message'], 'ok');
+    }
+  } while ($context['finished'] < 1);
+
+  // Output a summary message.
+  $operations = array_count_values($context['results']['operations']);
+  $details = [];
+  foreach ($operations as $op => $count) {
+    $details[] = $op . ' (' . $count . ')';
+  }
+  drush_log(dt('Action processing results: @results.', ['@results' => implode(', ', $details)]), 'ok');
+
+  // Display debug information.
+  if ($debug) {
+    _views_bulk_operations_timer($debug, 'execute');
+    drush_print(sprintf('Initialization time: %d ms.', _views_bulk_operations_timer($debug, 'init')));
+    drush_print(sprintf('Entity list generation time: %d ms.', _views_bulk_operations_timer($debug, 'list')));
+    drush_print(sprintf('Execution time: %d ms.', _views_bulk_operations_timer($debug, 'execute')));
+  }
+}
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.info.yml b/web/modules/views_bulk_operations/views_bulk_operations.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f78de2e72707e0f3ef065ad4280ad3303df73a33
--- /dev/null
+++ b/web/modules/views_bulk_operations/views_bulk_operations.info.yml
@@ -0,0 +1,13 @@
+type: module
+name: 'Views Bulk Operations'
+description: 'Adds an ability to perform bulk operations on selected entities from view results.'
+package: 'Views'
+# core: 8.x
+dependencies:
+  - drupal:views (>=8.4)
+
+# Information added by Drupal.org packaging script on 2018-07-02
+version: '8.x-2.4'
+core: '8.x'
+project: 'views_bulk_operations'
+datestamp: 1530516826
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.libraries.yml b/web/modules/views_bulk_operations/views_bulk_operations.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bed306fba03904e50c448fe7a3bafc52aaf913b8
--- /dev/null
+++ b/web/modules/views_bulk_operations/views_bulk_operations.libraries.yml
@@ -0,0 +1,20 @@
+frontUi:
+  version: 1.0
+  js:
+    js/frontUi.js: {}
+  css:
+    component:
+      css/frontUi.css: {}
+  dependencies:
+    - core/drupal
+    - core/jquery
+    - core/jquery.once
+
+adminUi:
+  version: 1.0
+  js:
+    js/adminUi.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery
+    - core/jquery.once
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.module b/web/modules/views_bulk_operations/views_bulk_operations.module
new file mode 100644
index 0000000000000000000000000000000000000000..74791d9f291200748b112a4f1bbadc2a70967dc4
--- /dev/null
+++ b/web/modules/views_bulk_operations/views_bulk_operations.module
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Allows operations to be performed on items selected in a view.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_views_data_alter().
+ */
+function views_bulk_operations_views_data_alter(&$data) {
+  $data['views']['views_bulk_operations_bulk_form'] = [
+    'title' => t('Views bulk operations'),
+    'help' => t("Process entities returned by the view with Views Bulk Operations' actions."),
+    'field' => [
+      'id' => 'views_bulk_operations_bulk_form',
+    ],
+  ];
+}
+
+/**
+ * Implements hook_help().
+ */
+function views_bulk_operations_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.views_bulk_operations':
+      $filepath = dirname(__FILE__) . '/README.txt';
+      if (file_exists($filepath)) {
+        $readme = file_get_contents($filepath);
+        $output = '<pre>' . $readme . '</pre>';
+
+        return $output;
+      }
+  }
+}
+
+/**
+ * Implements hook_preprocess_THEME().
+ */
+function views_bulk_operations_preprocess_views_view_table(&$variables) {
+  if (!empty($variables['view']->style_plugin->options['views_bulk_operations_enabled'])) {
+    // Add module own class to improve resistance to theme overrides.
+    $variables['attributes']['class'][] = 'vbo-table';
+  }
+}
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.routing.yml b/web/modules/views_bulk_operations/views_bulk_operations.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..36649a0ee7e7cf3a4232c537a1d1da3f6dfac3c2
--- /dev/null
+++ b/web/modules/views_bulk_operations/views_bulk_operations.routing.yml
@@ -0,0 +1,32 @@
+views_bulk_operations.execute_batch:
+  path: '/views-bulk-operations/execute/{view_id}/{display_id}'
+  defaults:
+    _controller: '\Drupal\views_bulk_operations\Controller\ViewsBulkOperationsController::execute'
+    _title: 'Views Bulk Operations batch starter'
+  requirements:
+    _views_bulk_operation_access: 'TRUE'
+views_bulk_operations.update_selection:
+  path: '/views-bulk-operations/ajax/{view_id}/{display_id}'
+  defaults:
+    _controller: '\Drupal\views_bulk_operations\Controller\ViewsBulkOperationsController::updateSelection'
+    _title: 'Views Bulk Operations multipage AJAX'
+  requirements:
+    _views_bulk_operation_access: 'TRUE'
+views_bulk_operations.execute_configurable:
+  path: '/views-bulk-operations/configure/{view_id}/{display_id}'
+  defaults:
+    _form: '\Drupal\views_bulk_operations\Form\ConfigureAction'
+    _title: 'Views Bulk Operations configure step'
+  requirements:
+    _views_bulk_operation_access: 'TRUE'
+  options:
+    _admin_route: TRUE
+views_bulk_operations.confirm:
+  path: '/views-bulk-operations/confirm/{view_id}/{display_id}'
+  defaults:
+    _form: '\Drupal\views_bulk_operations\Form\ConfirmAction'
+    _title: 'Views Bulk Operations confirm execution'
+  requirements:
+    _views_bulk_operation_access: 'TRUE'
+  options:
+    _admin_route: TRUE
diff --git a/web/modules/views_bulk_operations/views_bulk_operations.services.yml b/web/modules/views_bulk_operations/views_bulk_operations.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ef82b7ab8cc2abde7dabd4150d8afd515bc52e48
--- /dev/null
+++ b/web/modules/views_bulk_operations/views_bulk_operations.services.yml
@@ -0,0 +1,20 @@
+services:
+  views_bulk_operations.data:
+    class: Drupal\views_bulk_operations\Service\ViewsBulkOperationsViewData
+    arguments: ['@event_dispatcher']
+  views_bulk_operations.processor:
+    class: Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionProcessor
+    arguments: ['@views_bulk_operations.data', '@plugin.manager.views_bulk_operations_action', '@current_user', '@module_handler']
+  plugin.manager.views_bulk_operations_action:
+    class: Drupal\views_bulk_operations\Service\ViewsBulkOperationsActionManager
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@event_dispatcher']
+  views_bulk_operations.access:
+    class: Drupal\views_bulk_operations\Access\ViewsBulkOperationsAccess
+    arguments: ['@user.private_tempstore']
+    tags:
+      - { name: access_check, applies_to: _views_bulk_operation_access }
+  views_bulk_operations.view_data_provider:
+    class: Drupal\views_bulk_operations\EventSubscriber\ViewsBulkOperationsEventSubscriber
+    arguments: ['@views_bulk_operations.data']
+    tags:
+      - { name: event_subscriber }