From d78ebfb03a69cf9574ccb32421a1bfb20ae54768 Mon Sep 17 00:00:00 2001 From: Bachir Soussi Chiadmi Date: Tue, 6 Feb 2018 17:13:21 +0100 Subject: [PATCH] enabled workflow module - remains to configure it --- .../contrib/admin/workflow/CHANGELOG.txt | 7 + .../contrib/admin/workflow/LICENSE.txt | 339 +++++ .../config/install/workflow.settings.yml | 1 + .../block.block.workflowtransitionform.yml | 19 + ...n.change_a_node_to_next_workflow_state.yml | 21 + .../views.view.workflow_entity_history.yml | 713 +++++++++++ .../config/schema/workflow.schema.yml | 82 ++ .../config/schema/workflow.state.schema.yml | 27 + .../schema/workflow.transition.schema.yml | 27 + .../config/schema/workflow.views.schema.yml | 4 + .../install/workflow_access.settings.yml | 1 + .../config/schema/workflow_access.schema.yml | 6 + .../src/Form/WorkflowAccessRoleForm.php | 143 +++ .../src/Form/WorkflowAccessSettingsForm.php | 74 ++ .../workflow_access/workflow_access.info.yml | 13 + .../workflow_access/workflow_access.install | 41 + .../workflow_access.links.task.yml | 15 + .../workflow_access/workflow_access.module | 300 +++++ .../workflow_access.routing.yml | 21 + .../src/Form/WorkflowCleanupSettingsForm.php | 138 +++ .../workflow_cleanup.info.yml | 13 + .../workflow_cleanup.links.task.yml | 7 + .../workflow_cleanup/workflow_cleanup.module | 26 + .../workflow_cleanup.routing.yml | 7 + .../modules/workflow_devel/README.txt | 3 + .../workflow_devel/workflow_devel.info.yml | 14 + .../workflow_devel/workflow_devel.module | 143 +++ .../workflow_field/workflowfield.info.yml | 12 + .../src/Form/WorkflowTransitionRevertForm.php | 133 ++ .../src/WorkflowAccessControlHandler.php | 144 +++ .../src/WorkflowPermissions.php | 71 ++ .../workflow_operations.info.yml | 14 + .../workflow_operations.module | 97 ++ .../workflow_operations.permissions.yml | 2 + .../workflow_operations.routing.yml | 10 + .../src/Controller/WorkflowListBuilder.php | 104 ++ .../Controller/WorkflowStateListBuilder.php | 406 ++++++ .../src/Controller/WorkflowUiController.php | 24 + .../Form/WorkflowConfigTransitionFormBase.php | 191 +++ .../WorkflowConfigTransitionLabelForm.php | 103 ++ .../Form/WorkflowConfigTransitionRoleForm.php | 181 +++ .../modules/workflow_ui/workflow_ui.info.yml | 17 + .../modules/workflow_ui/workflow_ui.install | 24 + .../workflow_ui/workflow_ui.links.action.yml | 13 + .../workflow_ui/workflow_ui.links.menu.yml | 7 + .../workflow_ui/workflow_ui.links.task.yml | 28 + .../modules/workflow_ui/workflow_ui.module | 93 ++ .../workflow_ui/workflow_ui.routing.yml | 46 + .../WorkflowTransitionListController.php | 217 ++++ .../src/Element/WorkflowTransitionElement.php | 587 +++++++++ .../admin/workflow/src/Entity/Workflow.php | 444 +++++++ .../src/Entity/WorkflowConfigTransition.php | 222 ++++ .../WorkflowConfigTransitionInterface.php | 69 ++ .../workflow/src/Entity/WorkflowInterface.php | 169 +++ .../workflow/src/Entity/WorkflowManager.php | 468 +++++++ .../src/Entity/WorkflowManagerInterface.php | 173 +++ .../Entity/WorkflowScheduledTransition.php | 257 ++++ .../workflow/src/Entity/WorkflowState.php | 571 +++++++++ .../workflow/src/Entity/WorkflowStorage.php | 107 ++ .../src/Entity/WorkflowTransition.php | 1085 +++++++++++++++++ .../Entity/WorkflowTransitionInterface.php | 277 +++++ .../src/Form/WorkflowTransitionForm.php | 211 ++++ .../workflow/src/Form/WorkflowTypeForm.php | 247 ++++ .../Action/WorkflowNodeGivenStateAction.php | 79 ++ .../Action/WorkflowNodeNextStateAction.php | 71 ++ .../Plugin/Action/WorkflowStateActionBase.php | 232 ++++ .../Plugin/Block/WorkflowTransitionBlock.php | 85 ++ .../WorkflowDefaultFormatter.php | 206 ++++ .../Plugin/Field/FieldType/WorkflowItem.php | 421 +++++++ .../FieldWidget/WorkflowDefaultWidget.php | 233 ++++ .../Constraint/WorkflowFieldConstraint.php | 36 + .../WorkflowFieldConstraintValidator.php | 92 ++ .../src/Plugin/views/filter/WorkflowState.php | 55 + .../src/WorkflowAccessControlHandler.php | 146 +++ .../workflow/src/WorkflowPermissions.php | 79 ++ .../WorkflowScheduledTransitionViewsData.php | 22 + .../src/WorkflowTransitionListBuilder.php | 262 ++++ .../src/WorkflowTransitionViewsData.php | 176 +++ .../contrib/admin/workflow/workflow.api.php | 431 +++++++ .../contrib/admin/workflow/workflow.field.inc | 119 ++ .../contrib/admin/workflow/workflow.form.inc | 277 +++++ .../contrib/admin/workflow/workflow.info.yml | 17 + .../contrib/admin/workflow/workflow.install | 109 ++ .../admin/workflow/workflow.links.task.yml | 32 + .../contrib/admin/workflow/workflow.module | 705 +++++++++++ .../admin/workflow/workflow.permissions.yml | 10 + .../admin/workflow/workflow.routing.yml | 144 +++ .../admin/workflow/workflow.services.yml | 10 + .../contrib/admin/workflow/workflow.views.inc | 78 ++ sites/default/config/sync/core.extension.yml | 1 + ...n.change_a_node_to_next_workflow_state.yml | 25 + .../views.view.workflow_entity_history.yml | 716 +++++++++++ .../default/config/sync/workflow.settings.yml | 4 + 93 files changed, 13902 insertions(+) create mode 100644 sites/all/modules/contrib/admin/workflow/CHANGELOG.txt create mode 100644 sites/all/modules/contrib/admin/workflow/LICENSE.txt create mode 100644 sites/all/modules/contrib/admin/workflow/config/install/workflow.settings.yml create mode 100644 sites/all/modules/contrib/admin/workflow/config/optional/block.block.workflowtransitionform.yml create mode 100644 sites/all/modules/contrib/admin/workflow/config/optional/system.action.change_a_node_to_next_workflow_state.yml create mode 100644 sites/all/modules/contrib/admin/workflow/config/optional/views.view.workflow_entity_history.yml create mode 100644 sites/all/modules/contrib/admin/workflow/config/schema/workflow.schema.yml create mode 100644 sites/all/modules/contrib/admin/workflow/config/schema/workflow.state.schema.yml create mode 100644 sites/all/modules/contrib/admin/workflow/config/schema/workflow.transition.schema.yml create mode 100644 sites/all/modules/contrib/admin/workflow/config/schema/workflow.views.schema.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/install/workflow_access.settings.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/schema/workflow_access.schema.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessRoleForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessSettingsForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.info.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.install create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.links.task.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.module create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.routing.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/src/Form/WorkflowCleanupSettingsForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.info.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.links.task.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.module create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.routing.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_devel/README.txt create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_devel/workflow_devel.info.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_devel/workflow_devel.module create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_field/workflowfield.info.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/Form/WorkflowTransitionRevertForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowAccessControlHandler.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowPermissions.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.info.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.module create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.permissions.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.routing.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowListBuilder.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowStateListBuilder.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowUiController.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionFormBase.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionLabelForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionRoleForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.info.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.install create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.action.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.menu.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.task.yml create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.module create mode 100644 sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.routing.yml create mode 100644 sites/all/modules/contrib/admin/workflow/src/Controller/WorkflowTransitionListController.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Element/WorkflowTransitionElement.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/Workflow.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransition.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransitionInterface.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowInterface.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManager.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManagerInterface.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowScheduledTransition.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowState.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowStorage.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransition.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransitionInterface.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTransitionForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTypeForm.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeGivenStateAction.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeNextStateAction.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowStateActionBase.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Block/WorkflowTransitionBlock.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldFormatter/WorkflowDefaultFormatter.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldType/WorkflowItem.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Validation/Constraint/WorkflowFieldConstraint.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/Validation/Constraint/WorkflowFieldConstraintValidator.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/Plugin/views/filter/WorkflowState.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/WorkflowAccessControlHandler.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/WorkflowPermissions.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/WorkflowScheduledTransitionViewsData.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/WorkflowTransitionListBuilder.php create mode 100644 sites/all/modules/contrib/admin/workflow/src/WorkflowTransitionViewsData.php create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.api.php create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.field.inc create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.form.inc create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.info.yml create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.install create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.links.task.yml create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.module create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.permissions.yml create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.routing.yml create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.services.yml create mode 100644 sites/all/modules/contrib/admin/workflow/workflow.views.inc create mode 100644 sites/default/config/sync/system.action.change_a_node_to_next_workflow_state.yml create mode 100644 sites/default/config/sync/views.view.workflow_entity_history.yml create mode 100644 sites/default/config/sync/workflow.settings.yml diff --git a/sites/all/modules/contrib/admin/workflow/CHANGELOG.txt b/sites/all/modules/contrib/admin/workflow/CHANGELOG.txt new file mode 100644 index 000000000..97a4423fd --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/CHANGELOG.txt @@ -0,0 +1,7 @@ +Changes since 8.x-1.x +Issue #2351299 by johnv: Porting D7-version to D8 - numerous commits. + +Issue #2585093 by johnv: Fixed lost error message on Workflow UI - Transitions page. +Issue #2587165 by johnv: Port Workflow to D8: dropped features, clone support. +Issue #297898: Move "Workflow tab permissions" to permissions page' +Issue #2587171: Make permission "schedule workflow transition" specific per Workflow type \ No newline at end of file diff --git a/sites/all/modules/contrib/admin/workflow/LICENSE.txt b/sites/all/modules/contrib/admin/workflow/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/contrib/admin/workflow/config/install/workflow.settings.yml b/sites/all/modules/contrib/admin/workflow/config/install/workflow.settings.yml new file mode 100644 index 000000000..815b395bb --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/install/workflow.settings.yml @@ -0,0 +1 @@ +workflow_states_per_page: 20 diff --git a/sites/all/modules/contrib/admin/workflow/config/optional/block.block.workflowtransitionform.yml b/sites/all/modules/contrib/admin/workflow/config/optional/block.block.workflowtransitionform.yml new file mode 100644 index 000000000..1fb03029b --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/optional/block.block.workflowtransitionform.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - workflow + theme: + - bartik +id: workflowtransitionform +theme: bartik +region: sidebar_second +weight: -7 +provider: null +plugin: workflow_transition_form_block +settings: + id: workflow_transition_form_block + label: 'Workflow Transition form' + provider: workflow + label_display: visible +visibility: { } diff --git a/sites/all/modules/contrib/admin/workflow/config/optional/system.action.change_a_node_to_next_workflow_state.yml b/sites/all/modules/contrib/admin/workflow/config/optional/system.action.change_a_node_to_next_workflow_state.yml new file mode 100644 index 000000000..4dda8ce56 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/optional/system.action.change_a_node_to_next_workflow_state.yml @@ -0,0 +1,21 @@ +langcode: nl +status: true +dependencies: + module: + - action + - node + - workflow +id: change_a_node_to_next_workflow_state +label: 'Change a node to next Workflow state' +type: node +plugin: workflow_node_next_state_action +configuration: + label: 'Change a node to next Workflow state' + id: change_a_node_to_next_workflow_state + plugin: workflow_node_next_state_action + type: node + workflow_scheduling: + scheduled: '0' + comment: 'New state is set by a triggered Action.' + force: 0 + actions: { } diff --git a/sites/all/modules/contrib/admin/workflow/config/optional/views.view.workflow_entity_history.yml b/sites/all/modules/contrib/admin/workflow/config/optional/views.view.workflow_entity_history.yml new file mode 100644 index 000000000..5b3d5ba11 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/optional/views.view.workflow_entity_history.yml @@ -0,0 +1,713 @@ +langcode: en +status: true +dependencies: + module: + - workflow +id: workflow_entity_history +label: 'Workflow Entity history' +module: views +description: 'Enable this View to configure the history tab.' +tag: '' +base_table: workflow_transition_history +base_field: hid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + 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: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + 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 + tags: + previous: ‹‹ + next: ›› + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + hid: hid + timestamp: timestamp + field_name: field_name + from_sid: from_sid + to_sid: to_sid + uid: uid + comment: comment + operations: operations + info: + hid: + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + timestamp: + align: '' + separator: '' + empty_column: false + responsive: '' + field_name: + align: '' + separator: '' + empty_column: false + responsive: '' + from_sid: + align: '' + separator: '' + empty_column: false + responsive: '' + to_sid: + align: '' + separator: '' + empty_column: false + responsive: '' + uid: + align: '' + separator: '' + empty_column: false + responsive: '' + comment: + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + align: '' + separator: '' + empty_column: false + responsive: '' + default: hid + empty_table: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + hid: + id: hid + table: workflow_transition_history + field: hid + relationship: none + group_type: group + admin_label: '' + label: 'Transition ID' + exclude: true + 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 + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + 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 + entity_type: workflow_transition + entity_field: hid + plugin_id: field + timestamp: + id: timestamp + table: workflow_transition_history + field: timestamp + relationship: none + group_type: group + admin_label: '' + label: Timestamp + 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 + click_sort_column: value + type: timestamp + settings: + date_format: medium + custom_date_format: '' + timezone: '' + 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 + entity_type: workflow_transition + entity_field: timestamp + plugin_id: field + field_name: + id: field_name + table: workflow_transition_history + field: field_name + relationship: none + group_type: group + admin_label: '' + label: 'Field name' + exclude: true + 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 + click_sort_column: value + type: string + settings: + link_to_entity: false + 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 + entity_type: workflow_transition + entity_field: field_name + plugin_id: field + from_sid: + id: from_sid + table: workflow_transition_history + field: from_sid + relationship: none + group_type: group + admin_label: '' + label: 'From state' + 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 + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + 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 + entity_type: workflow_transition + entity_field: from_sid + plugin_id: field + to_sid: + id: to_sid + table: workflow_transition_history + field: to_sid + relationship: none + group_type: group + admin_label: '' + label: 'To state' + 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 + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + 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 + entity_type: workflow_transition + entity_field: to_sid + plugin_id: field + uid: + id: uid + table: workflow_transition_history + field: uid + relationship: none + group_type: group + admin_label: '' + label: 'User ID' + 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 + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + 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 + entity_type: workflow_transition + entity_field: uid + plugin_id: field + comment: + id: comment + table: workflow_transition_history + field: comment + relationship: none + group_type: group + admin_label: '' + label: 'Log message' + 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 + click_sort_column: value + type: basic_string + settings: { } + 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 + entity_type: workflow_transition + entity_field: comment + plugin_id: field + operations: + id: operations + table: workflow_transition_history + field: operations + relationship: none + group_type: group + admin_label: '' + label: 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 + destination: true + entity_type: null + entity_field: null + plugin_id: entity_operations + filters: { } + sorts: { } + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + entity_id: + id: entity_id + table: workflow_transition_history + field: entity_id + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + not: false + entity_type: workflow_transition + entity_field: entity_id + plugin_id: numeric + display_extenders: { } + title: 'Workflow history' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + tags: { } + workflow_history_tab: + display_plugin: embed + id: workflow_history_tab + display_title: 'Workflow history tab' + position: 1 + display_options: + display_extenders: { } + display_description: 'Replaces the transition list in the Workflow tab.' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + tags: { } diff --git a/sites/all/modules/contrib/admin/workflow/config/schema/workflow.schema.yml b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.schema.yml new file mode 100644 index 000000000..a14ae7ef1 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.schema.yml @@ -0,0 +1,82 @@ +# Schema for the configuration files of the Workflow module. +workflow.settings: + type: config_object + label: 'Workflow settings' + mapping: + workflow_states_per_page: + type: integer + label: 'Number of workflow states per page' + +workflow.workflow.*: + type: config_entity + label: 'Workflow settings' + mapping: + id: + type: string + label: 'ID' + status: + type: boolean + label: 'Status' + label: + type: label + label: 'Label' + module: + type: string + label: 'Module' + options: + type: mapping + label: 'Options' + mapping: + name_as_title: + type: integer + label: 'Use the workflow name as the title of the workflow form' + fieldset: + type: integer + label: 'Show the form in a fieldset' + options: + type: string + label: 'How to show the available states' + schedule_timezone: + type: integer + label: 'Show a timezone when scheduling a transition' + comment_log_node: + type: integer + label: 'Show comment on the Content edit form' + watchdog_log: + type: integer + label: 'Log watchdog messages upon state change' + +field.storage_settings.workflow: + type: mapping + label: 'Workflow settings' + mapping: + workflow_type: + type: string + label: 'Workflow' + allowed_values: + type: sequence + label: 'Allowed values list' + sequence: + type: mapping + label: 'Allowed value with label' + mapping: + value: + type: string + label: 'Value' + label: + type: label + label: 'Label' + allowed_values_function: + type: string + label: 'Allowed values function' + +field.field_settings.workflow: + type: mapping + label: 'Workflow settings' + +workflow.settings: + type: config_object + mapping: + workflow_states_per_page: + type: integer + label: 'Number of workflow states displayed per page.' diff --git a/sites/all/modules/contrib/admin/workflow/config/schema/workflow.state.schema.yml b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.state.schema.yml new file mode 100644 index 000000000..6c6121fbd --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.state.schema.yml @@ -0,0 +1,27 @@ +# Schema for the configuration files of the state from Workflow module. + +workflow.state.*: + type: config_entity + label: 'Workflow State' + mapping: + id: + type: string + label: 'ID' + status: + type: boolean + label: 'Status' + label: + type: label + label: 'Label' + module: + type: string + label: 'Module' + wid: + type: string + label: 'Machine name of the attached Workflow' + weight: + type: integer + label: 'Weight' + sysid: + type: integer + label: 'SysId' diff --git a/sites/all/modules/contrib/admin/workflow/config/schema/workflow.transition.schema.yml b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.transition.schema.yml new file mode 100644 index 000000000..cee1c6f05 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.transition.schema.yml @@ -0,0 +1,27 @@ +# Schema for the configuration files of the transitions for Workflow module. + +workflow.transition.*: + type: config_entity + label: 'Workflow Transition' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + module: + type: string + label: 'Module' + from_sid: + type: string + label: 'From workflow state' + to_sid: + type: string + label: 'To workflow state' + roles: + type: sequence + label: 'User roles' + sequence: + type: string + label: 'Role' diff --git a/sites/all/modules/contrib/admin/workflow/config/schema/workflow.views.schema.yml b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.views.schema.yml new file mode 100644 index 000000000..487769782 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/config/schema/workflow.views.schema.yml @@ -0,0 +1,4 @@ +# Schema for the views plugins of the Workflow module. + +views.filter.workflow_state: + type: views.filter.many_to_one diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/install/workflow_access.settings.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/install/workflow_access.settings.yml new file mode 100644 index 000000000..bfa09f363 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/install/workflow_access.settings.yml @@ -0,0 +1 @@ +workflow_access_priority: 0 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/schema/workflow_access.schema.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/schema/workflow_access.schema.yml new file mode 100644 index 000000000..e8567d8c9 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/config/schema/workflow_access.schema.yml @@ -0,0 +1,6 @@ +workflow_access.settings: + type: config_object + label: Settings + mapping: + workflow_access_priority: + type: integer diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessRoleForm.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessRoleForm.php new file mode 100644 index 000000000..73749f70c --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessRoleForm.php @@ -0,0 +1,143 @@ + t('State'), + 'view' => t('Roles who can view posts in this state'), + 'update' => t('Roles who can edit posts in this state'), + 'delete' => t('Roles who can delete posts in this state'), + ]; + return $header; + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row = []; + + $workflow = $this->workflow; + if ($workflow) { + /* @var $state WorkflowState */ + $state = $entity; + $sid = $state->id(); + + // A list of role names keyed by role ID, including the 'author' role. + // Only get the roles with proper permission + Author role. + $type_id = $workflow->id(); + $roles = workflow_get_user_role_names("create $type_id workflow_transition"); + + if ($state->isCreationState()) { + // No need to set perms on creation. + return []; + } + + $view = $update = $delete = []; + $count = 0; + foreach (workflow_access_get_workflow_access_by_sid($sid) as $rid => $access) { + $count++; + $view[$rid] = ($access['grant_view']) ? $rid : 0; + $update[$rid] = ($access['grant_update']) ? $rid : 0; + $delete[$rid] = ($access['grant_delete']) ? $rid : 0; + } + // Allow view grants by default for anonymous and authenticated users, + // if no grants were set up earlier. + if (!$count) { + $view = [ + AccountInterface::ANONYMOUS_ROLE => AccountInterface::ANONYMOUS_ROLE, + AccountInterface::AUTHENTICATED_ROLE => AccountInterface::AUTHENTICATED_ROLE, + ]; + } + + $row['label_new'] = [ + '#type' => 'value', + '#markup' => t('@label', ['@label' => $state->label()]), + ]; + $row['view'] = [ + '#type' => 'checkboxes', + '#options' => $roles, + '#default_value' => $view, + ]; + $row['update'] = [ + '#type' => 'checkboxes', + '#options' => $roles, + '#default_value' => $update, + ]; + $row['delete'] = [ + '#type' => 'checkboxes', + '#options' => $roles, + '#default_value' => $delete, + ]; + } + return $row; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + foreach ($form_state->getValue($this->entitiesKey) as $sid => $access) { + // @todo: not waterproof; can be done smarter, using elementchildren(). + if (!WorkflowState::load($sid)) { + continue; + } + + foreach ($access['view'] as $rid => $checked) { + $data[$rid] = [ + 'grant_view' => (!empty($access['view'][$rid])) ? (bool) $access['view'][$rid] : 0, + 'grant_update' => (!empty($access['update'][$rid])) ? (bool) $access['update'][$rid] : 0, + 'grant_delete' => (!empty($access['delete'][$rid])) ? (bool) $access['delete'][$rid] : 0, + ]; + } + workflow_access_insert_workflow_access_by_sid($sid, $data); + + // Update all nodes to reflect new settings. + node_access_needs_rebuild(TRUE); + } + + parent::submitForm($form, $form_state); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessSettingsForm.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessSettingsForm.php new file mode 100644 index 000000000..16f7fd356 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/src/Form/WorkflowAccessSettingsForm.php @@ -0,0 +1,74 @@ +config('workflow_access.settings'); + $weight = $config->get('workflow_access_priority'); + + $form['workflow_access'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => t('Workflow Access Settings'), + ]; + $form['workflow_access']['#tree'] = TRUE; + + $url = 'https://api.drupal.org/api/drupal/core%21modules%21node%21node.api.php/function/hook_node_access_records/8'; + $form['workflow_access']['workflow_access_priority'] = [ + '#type' => 'weight', + '#delta' => 10, + '#title' => t('Workflow Access Priority'), + '#default_value' => $weight, + '#description' => t('This sets the node access priority. Changing this + setting can be dangerous. If there is any doubt, leave it at 0. + Read the manual.', [':url' => $url]), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $weight = $form_state->getValues()['workflow_access']['workflow_access_priority']; + + $this->config('workflow_access.settings') + ->set('workflow_access_priority', $weight) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.info.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.info.yml new file mode 100644 index 000000000..0d3367ddc --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.info.yml @@ -0,0 +1,13 @@ +name: 'Workflow access' +description: 'Content access control based on workflows and roles. Depends on the node_access system, so only works for entities of type ''node''.' +dependencies: + - workflow +package: Workflow +# core: 8.x +type: module + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.0' +core: '8.x' +project: 'workflow' +datestamp: 1513552400 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.install b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.install new file mode 100644 index 000000000..57c658992 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.install @@ -0,0 +1,41 @@ +schema(); + $schema->dropTable('workflow_access'); + // Rebuild data. + node_access_needs_rebuild(TRUE); +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.links.task.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.links.task.yml new file mode 100644 index 000000000..a1bac44cd --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.links.task.yml @@ -0,0 +1,15 @@ +# Menu tasks for Workflow UI. + +workflow_access_settings: + route_name: workflow.access.settings + base_route: entity.workflow_type.collection + title: 'Access settings' + weight: 5 + appears_on: + - entity.workflow_type.collection + +entity.workflow_type.access_form: + title: 'Access' + route_name: entity.workflow_type.access_form + base_route: entity.workflow_type.edit_form + weight: -1 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.module b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.module new file mode 100644 index 000000000..ae82ed9a1 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.module @@ -0,0 +1,300 @@ +"; + $output .= t("WARNING: Use of the 'Edit any', 'Edit own', and even 'View + published content' permissions for the content type may override these + access settings. You may disable those permissions or + alter the priority of + the Workflow access module.", [':url' => $url->toString()] + ); + if (\Drupal::moduleHandler()->moduleExists('og')) { + $output .= '
'; + $output .= t('WARNING: Organic Groups (OG) is present and may interfere + with these settings.'); + //$output .= ' '; + //$url = \Drupal\Core\Url::fromUri('admin/config/group/settings'); // @todo D8: FIXME when OG module is ported. + //$output .= t("In particular, if Strict node access + // permissions is enabled, since this may override Workflow access + // settings.", [':url' => $url]); + $output .= t("In particular, if Strict node access + permissions is enabled, since this may override Workflow access + settings."); + } + break; + + default: + break; + } + return $output; +} + +/** + * Implements hook_workflow_operations(). + * + * Create action link for access form on EntityWorkflowUIController::overviewForm. + */ +function workflow_access_workflow_operations($op, EntityInterface $entity = NULL) { + $operations = []; + return $operations; +} + +/** + * Implements hook_entity_insert(). + * + * We use the Role weight as an id. + * In contrary to content_access module, that uses a 'content_access_roles_gids' + * config setting. + * @todo: determine the best way for D8. See also content_access.module + * The problem is that node_access table uses Int, whereas the Role Id is string. + */ +function workflow_access_user_role_insert(EntityInterface $entity) { + // Attend user to Rebuild data, because the weight of a role + // is the key for workflow_Access. + /** @var $entity \Drupal\user\RoleInterface */ + node_access_needs_rebuild(TRUE); +} + +/** + * Implements hook_access_entity_update(). + * + * @param EntityInterface $entity + */ +function workflow_access_user_role_update(EntityInterface $entity) { + // Attend user to Rebuild data, because the weight of a role + // is the key for workflow_Access. + /** @var $entity \Drupal\user\RoleInterface */ + if ($entity->getWeight() != $entity->original->getWeight()) { + // Role's weight has changed. + node_access_needs_rebuild(TRUE); + } +} + +/** + * Implements hook_node_grants(). + * + * Supply the workflow access grants. We are simply using + * roles as access lists, so rids translate directly to gids. + */ +function workflow_access_node_grants(AccountInterface $account, $op) { + $gids = []; + $roles = $account->getRoles(); + foreach ($roles as $role) { + // @todo D8: compare with content_access module. + $gids[] = workflow_access_get_role_gid($role); + } + + return [ + 'workflow_access' => $gids, + 'workflow_access_owner' => [$account->id()], + ]; +} + +/** + * Helper providing numeric id for role. + * Copied from content_access.module + */ +function workflow_access_get_role_gid($rid) { + // @todo D8: compare with content_access module. + // $config = \Drupal::configFactory()->getEditable('content_access.settings'); + // $roles_gids = $config->get('content_access_roles_gids'); + // return $roles_gids[$role]; + + /* @var $role Role */ + $role = \Drupal\user\Entity\Role::load($rid); + $weight = 100; // Avoid negative values. + if ($role) { + $weight += $role->getWeight(); + } + else { + // For Author, no role exists. + $weight -= 20; + } + + return $weight; +} + +/** + * Implements hook_node_access_records(). + * + * Returns a list of grant records for the passed in node object. + * This hook is invoked by function node_access_acquire_grants(). + */ +function workflow_access_node_access_records(\Drupal\node\NodeInterface $node) { + $grants = []; + $workflow_transitions_added = FALSE; + + //if (!$node->isPublished()) { + // // @todo: return; + //} + + $entity_type = $node->getEntityTypeId(); + + + // Only relevant for content with Workflow. + if (!isset($node->workflow_transitions)) { + $node->workflow_transitions = []; + // Sometimes, a node is saved without going through workflow_transition_form. + // E.g., + // - when saving a node with workflow_node programmatically with node_save(); + // - when changing a state on a node view page/history tab; + // - when rebuilding permissions via batch for Workflow Fields. + // In that case, we need to create the workflow_transitions ourselves to + // calculate the grants. + foreach (_workflow_info_fields($node, $entity_type) as $field) { + $field_name = $field->getName(); // Do not use id(). + $old_sid = $new_sid = $node->$field_name->value; + + // Create a dummy transition, just to set $node->workflow_transitions. + if ($old_sid) { + /* @var $transition WorkflowTransitionInterface */ + $transition = WorkflowTransition::create([$old_sid, 'field_name' => $field_name]); + $transition->setTargetEntity($node); + $transition->setValues($new_sid, $node->getOwnerId(), \Drupal::time()->getRequestTime(), ''); + + // Store the transition, so it can be easily fetched later on. + // Store in an array, to prepare for multiple workflow_fields per entity. + // This is a.o. used in hook_entity_update to trigger 'transition post'. + $node->workflow_transitions[$field_name] = $transition; + $workflow_transitions_added = TRUE; + } + } + } + + // Get 'author' of this entity. + // - Some entities (e.g., taxonomy_term) do not have a uid. + // But then again: node_access is only for nodes... + $uid = ($node->getOwnerId()) ? (int) $node->getOwnerId() : 0; + $priority = workflow_access_get_setting('workflow_access_priority'); + + $count = 0; + foreach ($node->workflow_transitions as $transition) { + // @todo: add support for +1 workflows per entity. + if ($count++ == 1) { + continue; + } + + $field_name = $transition->getFieldName(); + if ($current_sid = workflow_node_current_state($node, $field_name)) { + foreach (workflow_access_get_workflow_access_by_sid($current_sid) as $rid => $grant) { + $realm = ($uid > 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID) ? 'workflow_access_owner' : 'workflow_access'; + $gid = ($uid > 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID) ? $uid : workflow_access_get_role_gid($rid); + + // Anonymous ($uid == 0) author is not allowed for role 'author' (== -1). + // Both logically (Anonymous having more rights then authenticated) + // and technically: $gid must be a positive int. + if ($uid == 0 && $rid == WORKFLOW_ROLE_AUTHOR_RID) { + continue; + } + + $grants[] = [ + 'realm' => $realm, + 'gid' => $gid, + 'grant_view' => (int) $grant['grant_view'], + 'grant_update' => (int) $grant['grant_update'], + 'grant_delete' => (int) $grant['grant_delete'], + 'priority' => $priority, + // 'langcode' => $node->language(), + 'field_name' => $field_name, // Just for analysis and info. + ]; + } + } + } + if ($workflow_transitions_added == TRUE) { + unset($node->workflow_transitions); + } + + return $grants; +} + +/** + * Implements hook_node_access_explain(). + * + * This is a Devel Node Access hook. + */ +function workflow_access_node_access_explain($row) { + static $interpretations = []; + switch ($row->realm) { + case 'workflow_access_owner': + $interpretations[$row->gid] = t('Workflow access: author of the content may access'); + break; + + case 'workflow_access': + $roles = user_roles(); + $interpretations[$row->gid] = t('Workflow access: %role may access', ['%role' => $roles[$row->gid]]); + break; + } + return (!empty($interpretations[$row->gid]) ? $interpretations[$row->gid] : NULL); +} + +/** + * DB functions - all DB interactions are isolated here to make for easy updating should our schema change. + */ + +/** + * Given a sid, retrieve the access information and return the row(s). + */ +function workflow_access_get_workflow_access_by_sid($sid) { + $result = \Drupal::config('workflow_access.role')->get($sid); + if ($result == NULL) { + // Avoid errors in calling function when no data available. + $result = []; + } + return $result; +} + +/** + * Given a sid, delete all access data for this state. + */ +function workflow_access_delete_workflow_access_by_sid($sid) { + \Drupal::configFactory()->getEditable('workflow_access.role') + ->clear($sid) + ->save(); +} + +/** + * Given data, insert into workflow access - we never update. + */ +function workflow_access_insert_workflow_access_by_sid($sid, &$data, Config $config = NULL) { + $config = \Drupal::configFactory()->getEditable('workflow_access.role'); + $config + ->set($sid, $data) + ->save(); +} + +/** + * Get the module settings. + * + * @see WorkflowAccessSettingsForm::submitForm() + */ +function workflow_access_get_setting($value) { + $config = \Drupal::config('workflow_access.settings'); + $priority = $config->get($value); + return $priority; +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.routing.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.routing.yml new file mode 100644 index 000000000..f5bae05f6 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_access/workflow_access.routing.yml @@ -0,0 +1,21 @@ +# Declaration of Workflow Access routings. + +### Workflow settings form +workflow.access.settings: + path: '/admin/config/workflow/workflow/access' + defaults: + _form: '\Drupal\workflow_access\Form\WorkflowAccessSettingsForm' + _title: 'Access settings' + requirements: + _permission: 'administer workflow' + _module_dependencies: 'workflow_ui' + +entity.workflow_type.access_form: + path: '/admin/config/workflow/workflow/{workflow_type}/access' + defaults: + _title: 'Edit Workflow' + _title_callback: 'workflow_url_get_title' + _form: '\Drupal\workflow_access\Form\WorkflowAccessRoleForm' + requirements: + _permission: 'administer workflow' + _module_dependencies: 'workflow_ui' diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/src/Form/WorkflowCleanupSettingsForm.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/src/Form/WorkflowCleanupSettingsForm.php new file mode 100644 index 000000000..3529b0bad --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/src/Form/WorkflowCleanupSettingsForm.php @@ -0,0 +1,138 @@ +getWorkflow()) { + $orphans[$state->id()] = $state; + } + else { + // Is the state still active? + if (!$state->isActive()) { + $inactive[$state->id()] = $state; + } + } + } + + // Save the relevant states in an indexed array. + $form['#workflow_states'] = $orphans + $inactive; + + $form['no_workflow'] = [ + '#type' => 'details', + '#title' => t('Orphaned States'), + '#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE. + '#description' => t('These states no longer belong to an existing workflow.'), + '#tree' => TRUE, + ]; + foreach ($orphans as $sid => $state) { + $form['no_workflow'][$sid]['check'] = [ + '#type' => 'checkbox', + '#title' => $state->label(), + '#return_value' => $sid, + ]; + } + + $form['inactive'] = [ + '#type' => 'details', + '#title' => t('Inactive (Deleted) States'), + '#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE. + '#description' => t('These states belong to a workflow, but have been marked inactive (deleted).'), + '#tree' => TRUE, + ]; + foreach ($inactive as $sid => $state) { + $form['inactive'][$sid]['check'] = [ + '#type' => 'checkbox', + '#title' => $state->label() . ' (' . $state->getWorkflow()->label() . ')', + '#return_value' => $sid, + ]; + } + + $form['submit'] = [ + '#type' => 'submit', + '#value' => t('Delete selected states'), + ]; + + return $form; + } + + /** + * @inheritdoc + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $states = $form['#workflow_states']; + $values = $form_state->getValues(); + foreach (['no_workflow', 'inactive'] as $section) { + if (!isset($values[$section])) { + continue; + } + + foreach ($values[$section] as $sid => $data) { + if ($data['check']) { + /* @var $state WorkflowState */ + $state = $states[$sid]; + $state_name = $state->label(); + + // Delete any transitions this state is involved in. + $count = 0; + foreach (WorkflowConfigTransition::loadMultiple() as $config_transition) { + /* @var $config_transition WorkflowConfigTransition */ + if ($config_transition->getFromSid() == $sid || $config_transition->getToSid() == $sid) { + $config_transition->delete(); + $count++; + } + } + if ($count) { + drupal_set_message(t('@count transitions for the "@state" state have been deleted.', + ['@state' => $state_name, '@count' => $count])); + } + + // @todo: Remove history records too. + $count = 0; + // $count = db_delete('workflow_node_history')->condition('sid', $sid)->execute(); + if ($count) { + drupal_set_message(t('@count history records for the "@state" state have been deleted.', + ['@state' => $state_name, '@count' => $count])); + } + + $state->delete(); + drupal_set_message(t('The "@state" state has been deleted.', + ['@state' => $state_name])); + } + } + } + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.info.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.info.yml new file mode 100644 index 000000000..d88ca0eb1 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.info.yml @@ -0,0 +1,13 @@ +name: Workflow Clean Up +type: module +description: 'Cleans up Workflow cruft. (Do not enable in production sites.)' +package: Workflow +# core: 8.x +dependencies: + - workflow +configure: workflow.cleanup.settings +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.0' +core: '8.x' +project: 'workflow' +datestamp: 1513552400 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.links.task.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.links.task.yml new file mode 100644 index 000000000..3b9bd94e0 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.links.task.yml @@ -0,0 +1,7 @@ +# Menu tasks for Workflow Cleanup. + +workflow.cleanup.settings: + title: 'Cleanup' + route_name: workflow.cleanup.settings + base_route: entity.workflow_type.collection + weight: 7 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.module b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.module new file mode 100644 index 000000000..896b22fba --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_cleanup/workflow_cleanup.module @@ -0,0 +1,26 @@ +getPluginId()); + return hook_field_widget_form_alter($element, $form_state, $context); +} + +function workflow_devel_field_widget_workflow_default_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) { + // A hook specific for the 'workflow_default' widget. + // D7: This hook is introduced in Drupal 7.8. + // D8: This name is specified in the annotation of WorkflowDefaultWidget. + workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); + return hook_field_widget_workflow_default_form_alter($element, $form_state, $context); +} + +function workflow_devel_form_workflow_transition_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, ''); + return hook_form_workflow_transition_form_alter($form, $form_state, $form_id); +} + +function workflow_devel_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { + if (substr($form_id, 0, 8) == 'workflow') { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, ''); + } + return hook_form_alter($form, $form_state, $form_id); +} + +/** + * Hooks defined by core: Change the operations column in an Entity list. + * + * @see EntityListBuilder::getOperations() + * + * @return array + */ +function workflow_devel_entity_operation($entities) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); + $operations = []; + return $operations; +} + +function workflow_devel_entity_operation_alter(array $operations, EntityInterface $entity) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, $entity->getEntityTypeId(), $entity->id()); +} + +/** + * Hooks defined by core: hook_entity_CRUD. + * + * @see hook_entity_create(), hook_entity_update(), etc. + * @see hook_ENTITY_TYPE_create(), hook_ENTITY_TYPE_update(), etc. + */ +function workflow_devel_entity_create(EntityInterface $entity) { + //workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'create' , $entity->getEntityTypeId()); +} +function workflow_devel_entity_presave(EntityInterface $entity) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'presave', $entity->getEntityTypeId()); +} +function workflow_devel_entity_insert(EntityInterface $entity) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'insert', $entity->getEntityTypeId()); +} +function workflow_devel_entity_update(EntityInterface $entity) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'update', $entity->getEntityTypeId()); +} +function workflow_devel_entity_predelete(EntityInterface $entity) { + if (substr($entity->getEntityTypeId(), 0, 8) == 'workflow') { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'predelete', $entity->getEntityTypeId()); + } + return hook_entity_predelete($entity); +} +function workflow_devel_entity_delete(EntityInterface $entity) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete', $entity->getEntityTypeId()); + return hook_entity_delete($entity); +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_field/workflowfield.info.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_field/workflowfield.info.yml new file mode 100644 index 000000000..9a818e8b5 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_field/workflowfield.info.yml @@ -0,0 +1,12 @@ +name: 'Workflow Field' +description: 'A residual submodule. No need to enable this module for installations with workflow 8.x-1.0-beta6 and later.' +package: Workflow +type: module +# core: 8.x + +hidden: TRUE +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.0' +core: '8.x' +project: 'workflow' +datestamp: 1513552400 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/Form/WorkflowTransitionRevertForm.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/Form/WorkflowTransitionRevertForm.php new file mode 100644 index 000000000..73dbbad3c --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/Form/WorkflowTransitionRevertForm.php @@ -0,0 +1,133 @@ +entity; + $state = $transition->getFromState(); + + if ($state) { + $question = t('Are you sure you want to revert %title to the "@state" state?', [ + '@state' => $state->label(), + '%title' => $transition->label(), + ]); + return $question; + } + else { + \Drupal::logger('workflow_revert')->error('Invalid state', []); + drupal_set_message(t('Invalid transition. Your information has been recorded.'), 'error'); + //drupal_goto($return_uri); + } + + return []; + } + + public function getCancelUrl() { + /* @var $transition WorkflowTransitionInterface */ + $transition = $this->entity; + return new Url('entity.node.workflow_history', ['node' => $transition->getTargetEntityId(), 'field_name' => $transition->getFieldname()]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Revert'); + } + + /** + * {@inheritdoc} + */ + //public function getDescription() { + // return ''; + //} + + /** + * The fact that we need to overwrite this function, is an indicator that + * the Transition is not completely a complete Entity. + * + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + //return parent::copyFormValuesToEntity($entity, $form, $form_state); + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, \Drupal\Core\Form\FormStateInterface $form_state) { + + // If Rules is available, signal the reversion. + // @todo: move this Rules_invoke_event to hook outside this module. + if (\Drupal::moduleHandler()->moduleExists('rules')) { + rules_invoke_event('workflow_state_reverted', $this->entity); + } + + /* @var $transition WorkflowTransitionInterface */ + $transition = $this->prepareRevertedTransition($this->entity); + + // The entity will be updated when the transition is executed. Keep the + // original one for the confirmation message. + $previous_sid = $transition->getToSid(); + + // Force the transition because it's probably not valid. + $transition->force(TRUE); + $new_sid = workflow_execute_transition($transition, TRUE); + + $comment = ($previous_sid == $new_sid) ? 'State is reverted.' : 'State could not be reverted.'; + drupal_set_message(t($comment), 'warning'); + + $form_state->setRedirect('entity.node.workflow_history', [ + 'node' => $transition->getTargetEntityId(), + 'field_name' => $transition->getFieldName(), + ] + ); + } + + /** + * Prepares a transition to be reverted. + * + * @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition + * The transition to be reverted. + * + * @return \Drupal\workflow\Entity\WorkflowTransitionInterface + * The prepared transition ready to be stored. + */ + protected function prepareRevertedTransition(WorkflowTransitionInterface $transition) { + $user = \Drupal::currentUser(); + + $entity = $transition->getTargetEntity(); + $field_name = $transition->getFieldName(); + $current_sid = workflow_node_current_state($entity, $field_name); + $previous_sid = $transition->getFromSid(); + $comment = t('State reverted.'); + + $transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]); + $transition->setTargetEntity($entity); + $transition->setValues($previous_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment); + + return $transition; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowAccessControlHandler.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowAccessControlHandler.php new file mode 100644 index 000000000..9b0db2376 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowAccessControlHandler.php @@ -0,0 +1,144 @@ +entityTypeId = $entity_type->id(); + $this->entityType = $entity_type; + } + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type + ); + } + + /** + * Access check, to be called from + * - module.routing.yml + * - hook_entity_operation + * + * @param \Drupal\workflow_operations\WorkflowTransitionInterface|null $transition + * + * @return \Drupal\Core\Access\AccessResult + */ + public function revertAccess(WorkflowTransitionInterface $transition = NULL, AccountInterface $account = NULL, $return_as_object = TRUE) { + if ($transition) { + // Called from hook_entity_operation + } + else { + // Called from module.routing.yml + /* @var $transition WorkflowTransitionInterface */ + $route_match = \Drupal::routeMatch(); + $transition = $route_match->getParameter('workflow_transition'); + } + return $this->access($transition, 'revert', $account, $return_as_object); + } + + /** + * {@inheritdoc} + */ + public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::neutral(); + + $account = $user = workflow_current_user($account); + + // This is only for Edit/Delete transition. For Add/create, use createAccess. + switch ($entity->getEntityTypeId()) { + + case 'workflow_transition': + case 'workflow_scheduled_transition': + /* @var $transition WorkflowTransitionInterface */ + $transition = $entity; + + switch ($operation) { + case 'revert': + $is_owner = WorkflowManager::isOwner($user, $transition); + $type_id = $transition->getWorkflowId(); + if ($transition->getFromSid() == $transition->getToSid()) { + // No access for same state transitions. + $result = AccessResult::forbidden(); + } + elseif ($user->hasPermission("revert any $type_id workflow_transition")) { + // OK, add operation. + $result = AccessResult::allowed(); + } + elseif ($is_owner && $user->hasPermission("revert own $type_id workflow_transition")) { + // OK, add operation. + $result = AccessResult::allowed(); + } + else { + // No access. + $result = AccessResult::forbidden(); + } + break; + + default: + $result = parent::access($entity, $operation, $account, $return_as_object)->cachePerPermissions(); + break; + } // End of switch ($operation). + + break; // case + + default: // $entity_type + $result = AccessResult::forbidden(); + } // End of switch($entity->getEntityTypeId()). + + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + $result = parent::createAccess($entity_bundle, $account, $context, TRUE)->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $transition, $operation, AccountInterface $account) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + return parent::checkAccess($transition, $operation, $account); + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + return AccessResult::allowedIf($account->hasPermission('create ' . $entity_bundle . ' content'))->cachePerPermissions(); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowPermissions.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowPermissions.php new file mode 100644 index 000000000..c48b273c0 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/src/WorkflowPermissions.php @@ -0,0 +1,71 @@ +id(); + $type_params = ['%type_name' => $type->label()]; + + return [ + // D7->D8-Conversion of 'edit workflow comment' permission to "edit own $type_id transition" (@see NodePermissions::edit any/own content). + // D7->D8-Conversion of 'edit workflow comment' permission to "edit any $type_id transition" (@see NodePermissions::edit any/own content). + "edit own $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Edit own comments', $type_params), + 'description' => t('Edit the comment of own executed state transitions.'), + 'restrict access' => TRUE, + ], + "edit any $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Edit any comments', $type_params), + 'description' => t('Edit the comment of any executed state transitions.'), + 'restrict access' => TRUE, + ], + /* + // Workflow has no 'delete' permissions. + "delete own $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Delete own content', $type_params), + ], + "delete any $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Delete any content', $type_params), + ], + */ + // D7->D8-Conversion of 'revert workflow' permission to "revert any/own $type_id transition" + "revert own $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Revert own state transition', $type_params), + 'description' => t('Allow user to revert own last executed state transition on entity.'), + 'restrict access' => TRUE, + ], + "revert any $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Revert any state transition', $type_params), + 'description' => t('Allow user to revert any last executed state transition on entity.'), + 'restrict access' => TRUE, + ], + ]; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.info.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.info.yml new file mode 100644 index 000000000..3b0623fec --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.info.yml @@ -0,0 +1,14 @@ +name: 'Workflow operations' +description: "Adds lesser-used Transition permissions (edit, revert, delete permissions)." +package: Workflow +type: module +# core: 8.x + +dependencies: + - workflow + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.0' +core: '8.x' +project: 'workflow' +datestamp: 1513552400 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.module b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.module new file mode 100644 index 000000000..4f06f01e5 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.module @@ -0,0 +1,97 @@ + [ + 'group' => t('Workflow'), + 'label' => t('Workflow state reverted'), + 'variables' => rules_events_node_variables(t('updated content'), TRUE), + ], + ]; + return $events; +} + +/** + * Core hooks: Change the operations column in a Entity list. + * Add a 'revert' operation. + * + * @see EntityListBuilder::getOperations() + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * + * @return array + */ +function workflow_operations_entity_operation(\Drupal\Core\Entity\EntityInterface $entity) { + $operations = []; + + // Check correct entity type. + if (!in_array($entity->getEntityTypeId(), ['workflow_transition'])) { + return $operations; + } + + $user = workflow_current_user(); + /* @var $transition WorkflowTransitionInterface */ + $transition = $entity; + + // Check access, to avoid that user sees a revert link, but is not allowed to + // view the revert form. Use $first to check only once per page. + static $first = TRUE; + + if ($first) { + $handler = new WorkflowAccessControlHandler(); + $is_allowed = $handler->access($transition, 'revert', $user, FALSE); + if (!$is_allowed) { + // No access. We may try at next row. + return $operations; + } + } + + // Only mark the first row. + if ($first) { + // Some states are not fit to revert to. In each of these cases, prohibit + // to revert to an even older state. + if ((!$from_state = $transition->getFromState()) || !$from_state->isActive() || $from_state->isCreationState()) { + $first = FALSE; + } + else { + // Let's ask other modules if the reversion is allowed. Reversing old and new sid! + $permitted = \Drupal::moduleHandler()->invokeAll('workflow', ['transition revert', $transition, $user]); // @todo: add revert operation. + // Stop if a module says so. + if (!in_array(FALSE, $permitted, TRUE)) { + // Add the operation - it is not vetoed. + // @todo: Pluggability problem: the revert_form route is fixed in WorkflowTransition Annotation. Add to some alter() function. + $operations['revert'] = [ + 'title' => t('Revert to last state'), + 'url' => \Drupal\Core\Url::fromRoute('entity.workflow_transition.revert_form', ['workflow_transition' => $transition->id()]), + 'query' => \Drupal::destination()->getAsArray(), // Add destination. + 'weight' => 50, + ]; + + $first = FALSE; + } + } + } + + return $operations; +} + +/** + * Implements hook_entity_operation_alter + * This hook is not needed in this case. + */ +//function workflow_operations_entity_operation_alter(array $operations, \Drupal\Core\Entity\EntityInterface $entity) { +//} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.permissions.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.permissions.yml new file mode 100644 index 000000000..b1281dc5c --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.permissions.yml @@ -0,0 +1,2 @@ +permission_callbacks: + - \Drupal\workflow_operations\WorkflowPermissions::workflowTypePermissions diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.routing.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.routing.yml new file mode 100644 index 000000000..7c9501232 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_operations/workflow_operations.routing.yml @@ -0,0 +1,10 @@ +### Workflow Transition CRUD +entity.workflow_transition.revert_form: + path: '/workflow_transition/{workflow_transition}/revert' + defaults: + _entity_form: 'workflow_transition.revert' + _title: 'Revert Workflow transition' + requirements: +# _entity_access: 'workflow_transition.revert' + _custom_access: '\Drupal\workflow_operations\WorkflowAccessControlHandler::revertAccess' +# _access: 'true' diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowListBuilder.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowListBuilder.php new file mode 100644 index 000000000..59550bd79 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowListBuilder.php @@ -0,0 +1,104 @@ +t('ID'); + $header['label'] = $this->t('Label'); + $header['status'] = $this->t('Status'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /* @var $entity \Drupal\workflow\Entity\Workflow */ + $row['id'] = $entity->id(); + $row['label'] = $this->getLabel($entity); + $row['status'] = ''; // TODO $entity->getStatus(); + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + + /* @var $workflow \Drupal\workflow\Entity\Workflow */ + $workflow = $entity; + + // Do not delete a Workflow if it contains content. + if (isset($operations['delete']) && !$workflow->isDeletable()) { + unset ($operations['delete']); + } + + /** + * Allow modules to insert their own workflow operations to the list. + */ + // This is what EntityListBuilder::getOperations() does: + // $operations = $this->getDefaultOperations($entity); + // $operations += $this->moduleHandler()->invokeAll('entity_operation', [$entity]); + // $this->moduleHandler->alter('entity_operation', $operations, $entity); + + // In D8, the interface of below hook_workflow_operations has changed a bit. + // @see EntityListBuilder::getOperations, workflow_operations, workflow.api.php. + $operations += $this->moduleHandler()->invokeAll('workflow_operations', ['workflow', $workflow]); + + return $operations; + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + + /** + * Allow modules to insert their own top_action links to the list, like cleanup module. + * + * This is not done anymore via the workflow hook. + * Instead, for an example: + * @see workflow_ui.links.action.yml + * @see workflow.api.php under 'hook_workflow_operations'. + */ + // $top_actions = \Drupal::moduleHandler() + // ->invokeAll('workflow_operations', ['top_actions', NULL]); + // $top_actions_args = [ + // 'links' => $top_actions, + // 'attributes' => ['class' => ['inline', 'action-links']], + // ]; + + return $build; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowStateListBuilder.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowStateListBuilder.php new file mode 100644 index 000000000..c8f044403 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowStateListBuilder.php @@ -0,0 +1,406 @@ +id(); + + $entities = parent::load(); + foreach ($entities as $key => $entity) { + if (!isset($entity->wid) || $entity->wid != $wid) { + unset($entities[$key]); + } + } + + return $entities; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'workflow_state_form'; + //return parent::getFormId(); + } + + /** + * {@inheritdoc} + * + * Building the header and content lines for the contact list. + * + * Calling the parent::buildHeader() adds a column for the possible actions + * and inserts the 'edit' and 'delete' links as defined for the entity type. + */ + public function buildHeader() { + // The column 'weight' is added magically in the draggable EntityList. + // $header['weight'] = $this->t('Weight'); + // Some columns are not welcome in the list. + // $header['module'] = $this->t('Module'); + // $header['wid'] = $this->t('Workflow'); + // $header['sysid'] = $this->t('Sysid'); + // Column 'label' is manipulated in parent::buildForm(). So, we use 'label_new'. + // $header['label'] = $this->t('Label'); + $header['label_new'] = $this->t('Label'); + $header['id'] = $this->t('ID'); + $header['sysid'] = ''; + $header['status'] = $this->t('Active'); + $header['reassign'] = $this->t('Reassign'); + $header['count'] = $this->t('Count'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row = []; + + // Get the Workflow from the page. + /** @var $workflow \Drupal\workflow\Entity\Workflow */ + if (!$workflow = workflow_url_get_workflow()) { + workflow_debug( __FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + } + $wid = $url_wid = $workflow->id(); + + /** @var $entity \Drupal\workflow\Entity\WorkflowState */ + $state = $entity; + $sid = $state->id(); + $label = $state->label(); + $count = $state->count(); + + // Build select options for reassigning states. + // We put a blank state first for validation. + $state_options = ['' => ' ']; + $state_options += workflow_get_workflow_state_names($wid, FALSE); + + // Make it impossible to reassign to the same state that is disabled. + if ($state->isCreationState() || !$sid || !$state->isActive()) { + $current_state_options = []; + } + else { + $current_state = [$sid => $state_options[$sid]]; + $current_state_options = array_diff($state_options, $current_state); + } + + /* + * Build the Row. + */ + // The column 'weight' is added magically in the draggable EntityList. + // $row['weight'] = $state->weight; + // Some columns are not welcome in the list. + // $row['module'] = $state->getModule(); + // $row['wid'] = $state->getWorkflow(); + // Column 'label' is manipulated in parent::buildForm(). So, we use 'label_new'. + // $row['label'] = $state->label(); + $row['label_new'] = [ + '#markup' => $label, + '#type' => 'textfield', + '#size' => 30, + '#maxlength' => 255, + '#default_value' => $label, + '#title' => NULL, // This hides the red 'required' asterisk. + // '#required' => TRUE, + ]; + $row['id'] = [ + '#type' => 'machine_name', + '#title' => NULL, // This hides the red 'required' asterisk. + '#size' => 30, + '#description' => NULL, + '#disabled' => TRUE, // !$state->isNew(), + '#default_value' => $state->id(), + // N.B.: Keep machine_name in WorkflowState and ~ListBuilder aligned. + '#required' => FALSE, + // @TODO D8-port: enable machine_name: interactive WorkflowState element. + '#machine_name' => [ + 'exists' => [$this, 'exists'], // Local helper function, at the bottom of this class. +// 'source' => ['label_new'], + 'source' => ['states', $state->id(), 'label_new'], +// 'replace_pattern' =>'([^a-z0-9_]+)|(^custom$)', + 'replace_pattern' => '[^a-z0-9_()]+', // Added '()' characters from exclusion list since creation state has it. + 'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores.'), + ], + ]; + $row['sysid'] = [ + '#type' => 'value', + '#value' => $state->sysid, + ]; + $row['status'] = [ + '#type' => 'checkbox', + '#default_value' => $state->isActive(), + ]; + // The new value of states that are inactivated. + $row['reassign'] = [ + '#type' => 'select', + '#options' => $current_state_options, + ]; + $row['count'] = [ + '#type' => 'value', + '#value' => $count, + '#markup' => $count, + ]; + + // Disable some properties for Creation state. + if ($state->isCreationState()) { + $row['status']['#disabled'] = TRUE; + $row['reassign']['#type'] = 'hidden'; + $row['reassign']['#disabled'] = TRUE; + } + // New state and disabled states cannot be reassigned. + if (!$sid || !$state->isActive() || ($count == 0) ) { + $row['reassign']['#type'] = 'hidden'; + $row['reassign']['#disabled'] = TRUE; + } + // Disabled states cannot be renamed (and is a visual clue, too). + if (!$state->isActive()) { + $row['label_new']['#disabled'] = TRUE; + } + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get the Workflow from the page. + /** @var $workflow \Drupal\workflow\Entity\Workflow */ + if (!$workflow = workflow_url_get_workflow()) { + return $form; + } + $wid = $workflow->getWorkflowId(); + + $form = parent::buildForm($form, $form_state); + // Add a sticky header. + $form[$this->entitiesKey] += [ + '#sticky' => TRUE, + ]; + + // Build select options for reassigning states. + // We put a blank state first for validation. + $state_options = workflow_get_workflow_state_names($wid, FALSE); + // Is this the last state available? + $form['#last_mohican'] = (count($state_options) == 1); + + // Create a placeholder WorkflowState (It must NOT be saved to DB). Add it to the item list. + $sid = ''; + $placeholder = $workflow->createState($sid, FALSE); + $placeholder->set('label', ''); + $this->entities['placeholder'] = $placeholder; + $form['entities']['placeholder'] = $this->buildRow($placeholder); + + // Rename 'submit' button. + $form['actions']['submit']['#value'] = t('Save'); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + + /** @var $state \Drupal\workflow\Entity\WorkflowState */ + $state = $entity; + + // $workflow = $state->getWorkflow(); + + /** + * Allow modules to insert their own workflow operations to the list. + */ + // This is what EntityListBuilder::getOperations() does: + // $operations = $this->getDefaultOperations($entity); + // $operations += $this->moduleHandler()->invokeAll('entity_operation', [$entity]); + // $this->moduleHandler->alter('entity_operation', $operations, $entity); + + // In D8, the interface of below hook_workflow_operations has changed a bit. + // @see EntityListBuilder::getOperations, workflow_operations, workflow.api.php. + $operations += $this->moduleHandler()->invokeAll('workflow_operations', ['state', $state]); + + return $operations; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + + foreach ($form_state->getValue($this->entitiesKey) as $sid => $value) { + if (isset($this->entities[$sid])) { + /** @var $state WorkflowState */ + $state = $this->entities[$sid]; + + // Does user want to deactivate the state (reassign current content)? + if ($sid && $value['status'] == 0 && $state->isActive()) { + workflow_debug( __FILE__, __FUNCTION__, __LINE__, '', ''); // @todo D8-port: still test this snippet. + $args = ['%state' => $state->label()]; + // Does that state have content in it? + if ($value['count'] > 0 && empty($value['reassign'])) { + if ($form['#last_mohican']) { + $message = 'Since you are deleting the last available + workflow state in this workflow, all content items + which are in that state will have their workflow state + removed.'; + drupal_set_message($this->t($message, $args), 'warning'); + } + else { + $message = 'The %state state has content; you must + reassign the content to another state.'; + $form_state->setErrorByName("states'][$sid]['reassign'", $this->t($message, $args)); + } + } + } + + // Create the machine_name for new states. + // N.B.: Keep machine_name in WorkflowState and ~ListBuilder aligned. + if ($value['label_new'] && !$value['id']) { + //$message = 'Machine name is required.'; + //$form_state->setErrorByName('machine_name', $this->t($message)); + } + + } + } + return; + } + + /** + * {@inheritdoc} + * + * Overrides DraggableListBuilder::submitForm(). + * The WorkflowState entities are always saved. + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // parent::submitForm($form, $form_state); + + // Get the Workflow from the page. + /** @var $workflow \Drupal\workflow\Entity\Workflow */ + if (!$workflow = workflow_url_get_workflow()) { + return ; + } + + // The default min_weight is -10. Work with it. + $creation_weight = -11; + $max_weight = -9; + foreach ($form_state->getValue($this->entitiesKey) as $sid => $value) { + if (isset($this->entities[$sid])) { + /** @var $state WorkflowState */ + $state = $this->entities[$sid]; + + // Is the new state name empty? + if (empty($value['label_new'])) { + // No new state entered, so skip it. + continue; + } + + // Does user want to deactivate the state (reassign current content)? + if ($sid && $value['status'] == 0 && $state->isActive()) { + $new_sid = $value['reassign']; + $new_state = WorkflowState::load($new_sid); + + $args = [ + '%workflow' => $workflow->label(), + '%old_state' => $state->label(), + '%new_state' => isset($new_state) ? $new_state->label() : '', + ]; + + if ($value['count'] > 0) { + if ($form['#last_mohican']) { + $new_sid = NULL; // Do not reassign to new state. + $message = 'Removing workflow states from content in the %workflow.'; + drupal_set_message($this->t($message, $args)); + } + else { + // Prepare the state delete function. + $message = 'Reassigning content from %old_state to %new_state.'; + drupal_set_message($this->t($message, $args)); + } + } + // Delete the old state without orphaning content, move them to the new state. + $state->deactivate($new_sid); + + $message = $this->t('Deactivated workflow state %old_state in %workflow.', $args); + \Drupal::logger('workflow')->notice($message, []); + drupal_set_message($message); + } + + // Set a proper weight to the new state. + $max_weight = max($max_weight, $state->get($this->weightKey)); + + // Is this a new state? + if ($sid == 'placeholder' && empty(!$value['label_new'])) { + // New state, add it. + $state->set('id', $value['id']); + // Set a proper weight to the new state. + $state->set($this->weightKey, $max_weight + 1); + } + elseif ($value['sysid'] == WORKFLOW_CREATION_STATE) { + // Set a proper weight to the creation state. + $state->set($this->weightKey, $creation_weight); + } + else { + $state->set($this->weightKey, $value['weight']); + } + $state->set('label', $value['label_new']); + $state->set('status', $value['status']); + + $state->save(); + } + + } + drupal_set_message(t('The Workflow states have been updated.')); + + return; + } + + /** + * Validate duplicate machine names. Function registered in 'machine_name' + * form element. + * + * @param string $name + * @param array $element + * @param \Drupal\Core\Form\FormStateInterface $form_state + * + * @return bool + */ + function exists($name, array $element, FormStateInterface $form_state) { + $state_names = []; + foreach ($form_state->getValue($this->entitiesKey) as $sid => $value) { + $state_names[] = $value['id']; + } + $state_names = array_map('strtolower', $state_names); + $result = array_unique(array_diff_assoc($state_names, array_unique($state_names))); + + if (in_array($name, $result)) { + return TRUE; + } + return FALSE; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowUiController.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowUiController.php new file mode 100644 index 000000000..6287a7288 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Controller/WorkflowUiController.php @@ -0,0 +1,24 @@ + 'Workflow settings form is not implemented yet.', + ]; + return $element; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionFormBase.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionFormBase.php new file mode 100644 index 000000000..71b513d63 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionFormBase.php @@ -0,0 +1,191 @@ +type and $this->entitiesKey must be set in the var section. + + // Get the Workflow from the page. + $this->workflow = workflow_url_get_workflow(); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'workflow_config_transition_' . $this->type . '_form'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return []; + } + + /** + * {@inheritdoc} + * + * Create an $entity for every ConfigTransition. + */ + public function load() { + $entities = []; + + $entity_type = $this->entitiesKey; + $workflow = $this->workflow; + $states = $workflow->getStates($all = 'CREATION'); + + if ($states) { + /* @var $from_state WorkflowState */ + /* @var $to_state WorkflowState */ + switch ($entity_type) { + case 'workflow_state': + foreach ($states as $from_state) { + $from_sid = $from_state->id(); + $entities[$from_sid] = $from_state; + } + break; + + case 'workflow_config_transition': + foreach ($states as $from_state) { + $from_sid = $from_state->id(); + foreach ($states as $to_state) { + $to_sid = $to_state->id(); + + // Don't allow transition TO (creation). + if ($to_state->isCreationState()) { + continue; + } +// // Only allow transitions from $from_state. +// if ($state->id() <> $from_state->id()) { +// continue; +// } + + // Load existing config_transitions. Create if not found. + $config_transitions = $workflow->getTransitionsByStateId($from_sid, $to_sid); + if (!$config_transition = reset($config_transitions)) { + $config_transition = $workflow->createTransition($from_sid, $to_sid); + } + $entities[] = $config_transition; + } + } + break; + + default: + drupal_set_message(t('Improper type provided in load method.'), 'error'); + \Drupal::logger('workflow_ui')->notice('Improper type provided in load method.', []); + return $entities; + } + } + return $entities; + } + + /** + * {@inheritdoc} + */ +// abstract public function buildHeader(); + + /** + * {@inheritdoc} + */ +// abstract public function buildRow(EntityInterface $entity); + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = []; + + if (!$this->workflow) { + return $form; + } + + /* + * Begin of copied code DraggableListBuilder::buildForm() + */ + $form[$this->entitiesKey] = [ + '#type' => 'table', + '#header' => $this->buildHeader(), + '#sticky' => TRUE, + '#empty' => t('There is no @label yet.', ['@label' => 'Transition']), + '#tabledrag' => [['action' => 'order', 'relationship' => 'sibling', 'group' => 'weight', ], ], + ]; + + $this->entities = $this->load(); + $delta = 10; + // Change the delta of the weight field if have more than 20 entities. + if (!empty($this->weightKey)) { + $count = count($this->entities); + if ($count > 20) { + $delta = ceil($count / 2); + } + } + foreach ($this->entities as $entity) { + $row = $this->buildRow($entity); + if (isset($row['label'])) { + $row['label'] = ['#markup' => $row['label']]; + } + if (isset($row['weight'])) { + $row['weight']['#delta'] = $delta; + } + $form[$this->entitiesKey][$entity->id()] = $row; + } + /* + * End of copied code DraggableListBuilder::buildForm() + */ + + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionLabelForm.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionLabelForm.php new file mode 100644 index 000000000..090f3f968 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionLabelForm.php @@ -0,0 +1,103 @@ + t('Transition from'), + 'to' => t('Transition to'), + 'label_new' => t('label'), + 'config_transition' => '', + ]; + + return $header; + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row = []; + + $workflow = $this->workflow; + if ($workflow) { + /* @var $entity \Drupal\workflow\Entity\WorkflowConfigTransition */ + $config_transition = $entity; + + static $previous_from_sid = -1; + // Get transitions, sorted by weight of the old state. + $from_state = $config_transition->getFromState(); + $to_state = $config_transition->getToState(); + $from_sid = $from_state->id(); + + // Skip the transitions without any roles. + $skip = TRUE; + foreach ($config_transition->roles as $rid => $active) { + if ($active) { + $skip = FALSE; + } + } + if ($skip == TRUE && ($from_state != $to_state)) { + return $row; + } + + $row['from'] = [ + '#type' => 'value', + '#markup' => ($previous_from_sid != $from_sid) ? $from_state->label() : '"', + ]; + $row['to'] = [ + '#type' => 'value', + '#markup' => $to_state->label(), + ]; + $row['label_new'] = [ + '#type' => 'textfield', + '#default_value' => $config_transition->get('label'), + ]; + $row['config_transition'] = [ + '#type' => 'value', + '#value' => $config_transition, + ]; + + $previous_from_sid = $from_sid; + } + return $row; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + foreach ($form_state->getValue($this->entitiesKey) as $key => $value) { + $new_label = trim($value['label_new']); + $value['config_transition'] + ->set('label', $new_label) + ->save(); + } + + drupal_set_message(t('The transition labels have been saved.')); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionRoleForm.php b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionRoleForm.php new file mode 100644 index 000000000..19377ad87 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/src/Form/WorkflowConfigTransitionRoleForm.php @@ -0,0 +1,181 @@ +workflow; + $states = $workflow->getStates($all = 'CREATION'); + if ($states) { + $header['label_new'] = t('From \ To'); + + /* @var $state WorkflowState */ + foreach ($states as $state) { + // Don't allow transition TO (creation). + if (!$state->isCreationState()) { + $header[$state->id()] = t('@label', ['@label' => $state->label()]); + } + } + } + return $header; + } + + /** + * {@inheritdoc} + * + * Builds a row for the following table: + * Transitions, for example: + * 18 => [ + * 20 => [ + * 'author' => 1, + * 1 => 0, + * 2 => 1, + * ] + * ] + * means the transition from state 18 to state 20 can be executed by + * the content author or a user in role 2. The $transitions array should + * contain ALL transitions for the workflow. + */ + public function buildRow(EntityInterface $entity) { + $row = []; + + $workflow = $this->workflow; + if ($workflow) { + // Each $entity is a from-state. + /* @var $entity \Drupal\workflow\Entity\WorkflowState */ + $from_state = $entity; + $from_sid = $from_state->id(); + + /* @var $states WorkflowState[] */ + $states = $workflow->getStates($all = 'CREATION'); + if ($states) { + // Only get the roles with proper permission + Author role. + $type_id = $workflow->id(); + $roles = workflow_get_user_role_names("create $type_id workflow_transition"); + // Prepare default value for 'stay_on_this_state'. + $allow_all_roles = []; // array_combine (array_keys($roles) , array_keys($roles)); + + /* @var $state WorkflowState */ + foreach ($states as $state) { + $row['to'] = [ + '#type' => 'value', + '#markup' => t('@label', ['@label' => $from_state->label()]), + ]; + + /* @var $to_state WorkflowState */ + foreach ($states as $to_state) { + // Don't allow transition TO (creation). + if ($to_state->isCreationState()) { + continue; + } + // Only allow transitions from $from_state. + if ($state->id() <> $from_state->id()) { + continue; + } + $to_sid = $to_state->id(); + $stay_on_this_state = ($to_sid == $from_sid); + + // Load existing config_transitions. Create if not found. + $config_transitions = $workflow->getTransitionsByStateId($from_sid, $to_sid); + if (!$config_transition = reset($config_transitions)) { + $config_transition = $workflow->createTransition($from_sid, $to_sid); + } + + $row[$to_sid]['workflow_config_transition'] = ['#type' => 'value', '#value' => $config_transition, ]; + $row[$to_sid]['roles'] = [ + '#type' => $stay_on_this_state ? 'checkboxes' : 'checkboxes', + '#options' => $stay_on_this_state ? [] : $roles, + '#disabled' => $stay_on_this_state, + // When $stay_on_this_state, allow all roles. + '#default_value' => $stay_on_this_state ? $allow_all_roles : $config_transition->roles, + ]; + } + } + } + } + return $row; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $workflow = $this->workflow; + // Make sure 'author' is checked for (creation) -> [something]. + $creation_state = $workflow->getCreationState(); + + if (empty($form_state->getValue($this->entitiesKey))) { + $author_has_permission = TRUE; + } + else { + $author_has_permission = FALSE; + foreach ($form_state->getValue($this->entitiesKey) as $from_sid => $to_data) { + foreach ($to_data as $to_sid => $transition_data) { + if ($from_sid == $to_sid) { + // Same-state-transition do not count. + } + elseif (!empty($transition_data['roles'][WORKFLOW_ROLE_AUTHOR_RID])) { + $author_has_permission = TRUE; + break; + } + } + } + } + if (!$author_has_permission) { + $form_state->setErrorByName('id', t('Please give the author permission to go from %creation to at least one state!', + ['%creation' => $creation_state->label()])); + } + + return; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + foreach ($form_state->getValue($this->entitiesKey) as $from_sid => $to_data) { + foreach ($to_data as $transition_data) { + /* @var $config_transition WorkflowConfigTransition */ + if (isset($transition_data['workflow_config_transition'])) { + $config_transition = $transition_data['workflow_config_transition']; + $config_transition->roles = $transition_data['roles']; + $config_transition->save(); + } + else { + // Should not be possible. + // $config_transition = []; + } + } + } + + drupal_set_message(t('The workflow was updated.')); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.info.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.info.yml new file mode 100644 index 000000000..8bf924186 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.info.yml @@ -0,0 +1,17 @@ +name: 'Workflow UI' +description: 'Administrative UI for Workflow.' +package: Workflow +type: module +# core: 8.x +# version: VERSION + +configure: entity.workflow_type.collection + +dependencies: + - workflow + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.0' +core: '8.x' +project: 'workflow' +datestamp: 1513552400 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.install b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.install new file mode 100644 index 000000000..c2b2f663d --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.install @@ -0,0 +1,24 @@ + 'module-workflow']); + $message = t("Please review which roles may 'participate in workflows' + on the Permissions page.", + [':url' => $url->toString()]); + drupal_set_message($message); +} + +/** + * Drupal 8 updates. + */ +function workflow_ui_update_8001(&$sandbox) { +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.action.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.action.yml new file mode 100644 index 000000000..6e43aff42 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.action.yml @@ -0,0 +1,13 @@ +# Action links for Workflow UI. + +### Workflow related actions. +workflow_type_add_action: + route_name: entity.workflow_type.add_form + title: 'Add workflow' + weight: 1 + appears_on: + - entity.workflow_type.collection + +### Workflow State related actions. + +### Workflow Transition related actions. diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.menu.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.menu.yml new file mode 100644 index 000000000..66f173fed --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.menu.yml @@ -0,0 +1,7 @@ +# Menu links for Workflow UI. + +workflow.ui.workflows: + title: 'Workflow' + route_name: entity.workflow_type.collection + parent: system.admin_config_workflow + description: 'Workflow administration overview.' diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.task.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.task.yml new file mode 100644 index 000000000..573c11117 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.links.task.yml @@ -0,0 +1,28 @@ +# Menu tasks for Workflow UI. + +workflow.ui.settings: + title: 'Settings' + route_name: workflow.ui.settings + base_route: entity.workflow_type.collection + weight: 9 +workflow.ui.workflows: + title: 'Workflows' + route_name: entity.workflow_type.collection + base_route: entity.workflow_type.collection + weight: 1 + +workflow.ui.workflow_states: + title: 'States' + route_name: entity.workflow_state.collection + base_route: entity.workflow_type.edit_form + weight: -4 +workflow.ui.workflow_transitions: + title: 'Transitions' + route_name: entity.workflow_transition.collection + base_route: entity.workflow_type.edit_form + weight: -3 +workflow.ui.workflow_labels: + title: 'Transition labels' + route_name: entity.workflow_label.collection + base_route: entity.workflow_type.edit_form + weight: -2 diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.module b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.module new file mode 100644 index 000000000..bfce24235 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.module @@ -0,0 +1,93 @@ +' + . t("A state must be marked as active, to be available in the + workflow's transitions.") . '
' + . t("If you wish to inactivate a state that has content (i.e. count is + not zero), then you need to select a state to which to reassign that + content."); + + case 'entity.workflow_transition.collection': + $url = \Drupal\Core\Url::fromRoute('user.admin_permissions', [], + ['fragment' => 'module-workflow']); + return t('You are currently viewing the possible transitions to and from + workflow states. The state is shown in the left column; the state to be + moved to is to the right. For each transition, check the box next to + the role(s) that may initiate the transition. For example, if only the + "production editor" role may move content from Review state to the + Published state, check the box next to "production editor". The author + role is built in and refers to the user who authored the content.') + . '
' + . t("If not all roles are in the list, please review which roles may + 'participate in workflows' on the Permissions page. + On that page, uncheck the 'Authenticated user' role temporarily to + view the permissions of each separate role.", + [':url' => $url->toString()]); + + case 'entity.workflow_label.collection': + return t('You can add labels to transitions if you don\'t like the + standard state labels. They will modify the Workflow form options, so + specific workflow transitions can have their own labels, relative to + the beginning and ending states. Rather than showing the user a + workflow box containing options like "review required" as a state in + the workflow, it could say "move to the editing department for grammar + review".'); + } + return []; +} + +/** + * Helper function to determine Workflow from Workflow UI URL. + * + * @deprecated : @see workflow_url_get_workflow . + */ +function workflow_ui_url_get_workflow($url = '' ) { + return workflow_url_get_workflow($url); +} + +/** + * Helper function to determine the title of the page. + * + * @deprecated : @see workflow_url_get_title . + */ +function workflow_ui_url_get_title() { + return workflow_url_get_title(); +} + +/** + * Helper function to determine Workflow from Workflow UI URL. + * + * @deprecated : @see workflow_url_get_form_type . + */ +function workflow_ui_url_get_form_type($url = '' ) { + return workflow_url_get_form_type($url); +} diff --git a/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.routing.yml b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.routing.yml new file mode 100644 index 000000000..93c16fc6b --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/modules/workflow_ui/workflow_ui.routing.yml @@ -0,0 +1,46 @@ +# Declaration of Workflow UI routings. + +# copy from WorkflowUIController.php +# https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21entity.api.php/group/entity_api/8 + +### Workflow settings form +workflow.ui.settings: + path: '/admin/config/workflow/workflow/settings' + defaults: + _controller: '\Drupal\workflow_ui\Controller\WorkflowUiController::settingsForm' + _title: 'Workflow settings' + requirements: + _permission: 'administer workflow' + + +### Workflow States +entity.workflow_state.collection: + path: '/admin/config/workflow/workflow/{workflow_type}/states' + defaults: + _entity_list: 'workflow_state' +# _controller: '\workflow\EntityWorkflowUIController::adminOverview' + _title: 'Edit Workflow' + _title_callback: 'workflow_url_get_title' + requirements: + _permission: 'administer workflow' + + +### Workflow Transitions +entity.workflow_transition.collection: + path: '/admin/config/workflow/workflow/{workflow_type}/transition_roles' + defaults: + _form: 'Drupal\workflow_ui\Form\WorkflowConfigTransitionRoleForm' + _title: 'Edit Workflow' + _title_callback: 'workflow_url_get_title' + requirements: + _permission: 'administer workflow' + +### Workflow Labels +entity.workflow_label.collection: + path: '/admin/config/workflow/workflow/{workflow_type}/transition_labels' + defaults: + _form: 'Drupal\workflow_ui\Form\WorkflowConfigTransitionLabelForm' + _title: 'Edit Workflow' + _title_callback: 'workflow_url_get_title' + requirements: + _permission: 'administer workflow' diff --git a/sites/all/modules/contrib/admin/workflow/src/Controller/WorkflowTransitionListController.php b/sites/all/modules/contrib/admin/workflow/src/Controller/WorkflowTransitionListController.php new file mode 100644 index 000000000..0329b8ac3 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Controller/WorkflowTransitionListController.php @@ -0,0 +1,217 @@ +dateFormatter = $date_formatter; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('date.formatter'), + $container->get('renderer') + ); + } + + /** + * Generates an overview table of older revisions of a node, + * but only if this::historyAccess() allows it. + * + * @param EntityInterface $node + * A node object. + * @return array An array as expected by drupal_render(). + * An array as expected by drupal_render(). + */ + public function historyOverview(EntityInterface $node = NULL) { + $form = []; + + /* + * Get data from parameters. + */ + + // @todo D8-port: make Workflow History tab happen for every entity_type. + // For workflow_tab_page with multiple workflows, use a separate view. See [#2217291]. + // @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController. + // workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + // ATM it only works for Nodes and Terms. + // This is a hack. The Route should always pass an object. + // On view tab, $entity is object, + // On workflow tab, $entity is id(). + // Get the entity for this form. + if (!$entity = workflow_url_get_entity($node)) { + return $form; + } + + /* + * Get derived data from parameters. + */ + if (!$field_name = workflow_get_field_name($entity, workflow_url_get_field_name())) { + return $form; + } + + /* + * Step 1: generate the Transition Form. + */ + // Add the WorkflowTransitionForm to the page. + $form = WorkflowManager::getWorkflowTransitionForm($entity, $field_name); + + /* + * Step 2: generate the Transition History List. + */ + $view = NULL; + $moduleHandler = \Drupal::service('module_handler'); + if ($moduleHandler->moduleExists('views')){ + $view = Views::getView('workflow_entity_history'); + } + if (is_object($view) && $view->storage->status()) { + // Add the history list from configured Views display. + $args = [$entity->id()]; + $view->setArguments($args); + $view->setDisplay('workflow_history_tab'); + $view->preExecute(); + $view->execute(); + $form['table'] = $view->buildRenderable(); + } + else { + // @deprecated. Use the Views display above. + // Add the history list from programmed WorkflowTransitionListController. + $entity_type = 'workflow_transition'; + // $form = $this->listing('workflow_transition'); + $list_builder = $this->entityTypeManager()->getListBuilder($entity_type); + // Add the Node explicitly, since $list_builder expects a Transition. + $list_builder->workflow_entity = $entity; + $form += $list_builder->render(); + + } + + /* + * Finally: sort the elements (overriding their weight). + */ + // $form['#weight'] = 10; + $form['actions']['#weight'] = 100; + $form['table']['#weight'] = 201; + + return $form; + } + + /** + * Menu access control callback. Checks access to Workflow tab. + * + * This used to be D7-function workflow_tab_access($user, $entity). + * + * The History tab should not be used with multiple workflows per entity. + * Use the dedicated view for this use case. + * @todo D8: remove this in favour of View 'Workflow history per entity'. + * @todo D8-port: make this workflow for non-Node entity types. + * + * @param \Drupal\Core\Session\AccountInterface $account + * Run access checks for this account. + * + * @return \Drupal\Core\Access\AccessResult + */ + public function historyAccess(AccountInterface $account) { + static $access = []; + + $uid = ($account) ? $account->id() : -1; + + // @todo D8-port: make Workflow History tab happen for every entity_type. + // @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController. + // ATM it only works for Nodes and Terms. + // This is a hack. The Route should always pass an object. + // On view tab, $entity is object, + // On workflow tab, $entity is id(). + // Get the entity for this form. + $entity = workflow_url_get_entity(); + + /* @var $entity EntityInterface */ + // Figure out the $entity's bundle and id. + $entity_type = $entity->getEntityTypeId(); + $entity_bundle = $entity->bundle(); + $entity_id = ($entity) ? $entity->id() : ''; + $field_name = workflow_url_get_field_name(); + + if (isset($access[$uid][$entity_type][$entity_id][$field_name ? $field_name : 'no_field'])) { + return $access[$uid][$entity_type][$entity_id][$field_name ? $field_name : 'no_field']; + } + + $access_result = AccessResult::forbidden(); + + // When having multiple workflows per bundle, use Views display + // 'Workflow history per entity' instead! + $fields = _workflow_info_fields($entity, $entity_type, $entity_bundle, $field_name); + if (!$fields) { + return AccessResult::forbidden(); + } + else { + // @todo: Keep below code aligned between WorkflowState, ~Transition, ~TransitionListController + $uid = ($account) ? $account->id() : -1; + $entity_id = ($entity) ? $entity->id() : ''; + // Determine if user is owner of the entity. + $is_owner = WorkflowManager::isOwner($account, $entity); + + /** + * Determine if user has Access. Fill the cache. + */ + // @todo: what to do with multiple workflow_fields per bundle? Use Views instead! Or introduce a setting. + // @todo D8-port: workflow_tab_access: use proper 'WORKFLOW_TYPE' permissions + foreach ($fields as $definition) { + $type_id = $definition->getSetting('workflow_type'); + if ($account->hasPermission("access any $type_id workflow_transion overview")) { + $access_result = AccessResult::allowed(); + } + elseif ($is_owner && $account->hasPermission("access own $type_id workflow_transion overview")) { + $access_result = AccessResult::allowed(); + } + elseif ($account->hasPermission('administer nodes')) { + $access_result = AccessResult::allowed(); + } + $access[$uid][$entity_type][$entity_id][$field_name ? $field_name : 'no_field'] = $access_result; + } + } + return $access_result; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Element/WorkflowTransitionElement.php b/sites/all/modules/contrib/admin/workflow/src/Element/WorkflowTransitionElement.php new file mode 100644 index 000000000..3c25ddbe9 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Element/WorkflowTransitionElement.php @@ -0,0 +1,587 @@ +getFieldName(); + $workflow = $transition->getWorkflow(); + $wid = $transition->getWorkflowId(); + $force = $transition->isForced(); + $entity = $transition->getTargetEntity(); + $entity_type = $transition->getTargetEntityTypeId(); + $entity_id = $transition->getTargetEntityId(); + + if ($transition->isExecuted()) { + // We are editing an existing/executed/not-scheduled transition. + // Only the comments may be changed! + + $current_sid = $from_sid = $transition->getFromSid(); + // The states may not be changed anymore. + $to_state = $transition->getToState(); + $options = [$to_state->id() => $to_state->label()]; + // We need the widget to edit the comment. + $show_widget = TRUE; + $default_value = $transition->getToSid(); + } + elseif ($entity) { + // Normal situation: adding a new transition on an new/existing entity. + + // Get the scheduling info, only when updating an existing entity. + // This may change the $default_value on the Form. + // Technically you could have more than one scheduled transition, but + // this will only add the soonest one. + // @todo: Read the history with an explicit langcode? + $langcode = ''; // $entity->language()->getId(); + if ($entity_id && $scheduled_transition = WorkflowScheduledTransition::loadByProperties($entity_type, $entity_id, [], $field_name, $langcode)) { + $transition = $scheduled_transition; + } + + $current_sid = $from_sid = $transition->getFromSid(); + $current_state = $from_state = $transition->getFromState(); + $options = ($current_state) ? $current_state->getOptions($entity, $field_name, $user, FALSE) : []; + $show_widget = ($from_state) ? $from_state->showWidget($entity, $field_name, $user, FALSE) : []; + $default_value = $from_sid; + $default_value = ($from_state && $from_state->isCreationState()) ? $workflow->getFirstSid($entity, $field_name, $user, FALSE) : $default_value; + $default_value = ($transition->isScheduled()) ? $transition->getToSid() : $default_value; + } + elseif (!$entity) { + // Sometimes, no entity is given. We encountered the following cases: + // - D7: the Field settings page, + // - D7: the VBO action form; + // - D7/D8: the Advance Action form on admin/config/system/actions; + // If so, show all options for the given workflow(s). + if (!$temp_state = $transition->getFromState()) { + $temp_state = $transition->getToState(); + } + $options = ($temp_state) + ? $temp_state->getOptions($entity, $field_name, $user, FALSE) + : workflow_get_workflow_state_names($wid, $grouped = TRUE); + $show_widget = TRUE; + $current_sid = $transition->getToSid(); // @todo + $default_value = $from_sid = $transition->getToSid(); // @todo + } + else { + // We are in trouble! A message is already set in workflow_node_current_state(). + $options = []; + $current_sid = 0; + $show_widget = FALSE; + $default_value = FALSE; + } + + // Fetch the form ID. This is unique for each entity, to allow multiple form per page (Views, etc.). + // Make it uniquer by adding the field name, or else the scheduling of + // multiple workflow_fields is not independent of each other. + // If we are indeed on a Transition form (so, not a Node Form with widget) + // then change the form id, too. + $form_id = self::getFormId(); + + /* + * Output: generate the element. + */ + // Get settings from workflow. @todo: implement default_settings. + if ($workflow) { + $workflow_settings = $workflow->options; + } + else { + // @todo D8-port: now only tested with Action. + $workflow_settings = [ + 'name_as_title' => 0, + 'options' => "radios", + 'schedule_timezone' => 1, + 'comment_log_node' => "1", + 'watchdog_log' => TRUE, + ]; + } + // Current sid and default value may differ in a scheduled transition. + + $workflow_settings['comment'] = $workflow_settings['comment_log_node']; // 'comment_log_tab' is removed; + + // Capture settings to format the form/widget. + $settings_title_as_name = !empty($workflow_settings['name_as_title']); + $settings_fieldset = isset($workflow_settings['fieldset']) ? $workflow_settings['fieldset'] : 0; + $settings_options_type = $workflow_settings['options']; + + // Display scheduling form if user has permission. + // Not shown on new entity (not supported by workflow module, because that + // leaves the entity in the (creation) state until scheduling time.) + // Not shown when editing existing transition. + $type_id = ($workflow) ? $workflow->id() : ''; // Might be empty on Action configuration. + $settings_schedule = !$transition->isExecuted() && $user->hasPermission("schedule $type_id workflow_transition"); + if ($settings_schedule) { + //@todo D8-port: check below code: form on VBO. + // workflow_debug( __FILE__ , __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + $step = $form_state->getValue('step'); + if (isset($step) && ($form_state->getValue('step') == 'views_bulk_operations_config_form')) { + // On VBO 'modify entity values' form, leave field settings. + $settings_schedule = TRUE; + } + else { + // ... and cannot be shown on a Content add page (no $entity_id), + // ...but can be shown on a VBO 'set workflow state to..'page (no entity). + $settings_schedule = !($entity && !$entity_id); + } + } + + $settings_schedule_timezone = !empty($workflow_settings['schedule_timezone']); + // Show comment, when both Field and Instance allow this. + $settings_comment = $workflow_settings['comment']; + + $transition_is_scheduled = $transition->isScheduled(); + // Save the current value of the entity in the form, for later Workflow-module specific references. + // We add prefix, since #tree == FALSE. + $element['workflow_transition'] = [ + '#type' => 'value', + '#value' => $transition, + ]; + + // Decide if we show a widget or a formatter. + // There is no need for a widget when the only option is the current sid. + + // Add a state formatter before the rest of the form, + // when transition is scheduled or widget is hidden. + if ( (!$show_widget) || $transition_is_scheduled || $transition->isExecuted()) { + $element['workflow_current_state'] = workflow_state_formatter($entity, $field_name, $current_sid); + // Set a proper weight, which works for Workflow Options in select list AND action buttons. + $element['workflow_current_state']['#weight'] = -0.005; + } + + $element['#tree'] = TRUE; + // Add class following node-form pattern (both on form and container). + $workflow_type_id = ($workflow) ? $workflow->id() : ''; + $element['#attributes']['class'][] = 'workflow-transition-' . $workflow_type_id . '-container'; + $element['#attributes']['class'][] = 'workflow-transition-container'; + if (!$show_widget) { + // Show no widget. + $element['to_sid']['#type'] = 'value'; + $element['to_sid']['#value'] = $default_value; + $element['to_sid']['#options'] = $options; // In case action buttons need them. + $element['comment']['#type'] = 'value'; + $element['comment']['#value'] = ''; + + return $element; // <-- exit. + } + + // @todo: repair the usage of $settings_title_as_name: no container if no details (schedule/comment). + // Prepare a UI wrapper. This might be a fieldset. + if ($settings_fieldset == 0) { // Use 'container'. + $element += [ + '#type' => 'container', + ]; + } + else { + $element += [ + '#type' => 'details', + '#collapsible' => TRUE, + '#open' => ($settings_fieldset == 2) ? FALSE : TRUE, + ]; + } + + $element['field_name'] = [ + '#type' => 'select', + '#title' => t('Field name'), + '#description' => t('Choose the field name.'), + '#access' => FALSE, // Only show on VBO/Actions screen. + '#options' => workflow_get_workflow_field_names($entity), + '#default_value' => $field_name, + '#required' => TRUE, + '#weight' => -20, + ]; + $element['force'] = [ + '#type' => 'checkbox', + '#title' => t('Force transition'), + '#description' => t('If this box is checked, the new state will be assigned even if workflow permissions disallow it.'), + '#access' => FALSE, // Only show on VBO/Actions screen. + '#default_value' => $force, + '#weight' => -19, + ]; + + // This overrides BaseFieldDefinition. @todo: apply for form and widget. + // The 'options' widget. May be removed later if 'Action buttons' are chosen. + // The help text is not available for container. Let's add it to the + // State box. N.B. it is empty on Workflow Tab, Node View page. + $help_text = isset($element['#description']) ? $element['#description'] : ''; + // This overrides BaseFieldDefinition. @todo: apply for form and widget. + $element['to_sid'] = [ + '#type' => ($wid) ? $settings_options_type : 'select', // Avoid error with grouped options. + '#title' => ($settings_title_as_name && !$transition->isExecuted()) + ? t('Change @name state', ['@name' => $workflow->label()]) + : t('Target state'), + '#access' => TRUE, + '#options' => $options, + // '#parents' => array('workflow'), + '#default_value' => $default_value, + '#description' => $help_text, + ]; + + // Display scheduling form under certain conditions. + if ($settings_schedule == TRUE) { + $timezone = $user->getTimeZone(); + + $timezone_options = array_combine(timezone_identifiers_list(), timezone_identifiers_list()); + $timestamp = $transition ? $transition->getTimestamp() : \Drupal::time()->getRequestTime(); + $hours = (!$transition_is_scheduled) ? '00:00' : \Drupal::service('date.formatter')->format($timestamp, 'custom', 'H:i', $timezone); + // Add a container, so checkbox and time stay together in extra fields. + $element['workflow_scheduling'] = [ + '#type' => 'container', + '#tree' => TRUE, + ]; + $element['workflow_scheduling']['scheduled'] = [ + '#type' => 'radios', + '#title' => t('Schedule'), + '#options' => [ + '0' => t('Immediately'), + '1' => t('Schedule for state change'), + ], + '#default_value' => $transition_is_scheduled ? '1' : '0', + '#attributes' => [ + // 'id' => 'scheduled_' . $form_id, + 'class' => [Html::getClass('scheduled_' . $form_id)], + ], + ]; + $element['workflow_scheduling']['date_time'] = [ + '#type' => 'details', // 'container', + '#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE. + '#attributes' => ['class' => ['container-inline']], + '#prefix' => '
', + '#suffix' => '
', + '#states' => [ + //'visible' => array(':input[id="' . 'scheduled_' . $form_id . '"]' => array('value' => '1')), + 'visible' => ['input.' . Html::getClass('scheduled_' . $form_id) => ['value' => '1']], + ], + ]; + $element['workflow_scheduling']['date_time']['workflow_scheduled_date'] = [ + '#type' => 'date', + '#prefix' => t('At'), + '#default_value' => implode( '-', [ + 'year' => date('Y', $timestamp), + 'month' => date('m', $timestamp), + 'day' => date('d', $timestamp), + ] + ) + ]; + $element['workflow_scheduling']['date_time']['workflow_scheduled_hour'] = [ + '#type' => 'textfield', + '#title' => t('Time'), + '#maxlength' => 7, + '#size' => 6, + '#default_value' => $hours, + '#element_validate' => ['_workflow_transition_form_element_validate_time'], // @todo D8-port: this is not called. + ]; + $element['workflow_scheduling']['date_time']['workflow_scheduled_timezone'] = [ + '#type' => $settings_schedule_timezone ? 'select' : 'hidden', + '#title' => t('Time zone'), + '#options' => $timezone_options, + '#default_value' => [$timezone => $timezone], + ]; + $element['workflow_scheduling']['date_time']['workflow_scheduled_help'] = [ + '#type' => 'item', + '#prefix' => '
', + '#description' => t('Please enter a time. + If no time is included, the default will be midnight on the specified date. + The current time is: @time.', ['@time' => \Drupal::service('date.formatter')->format(\Drupal::time()->getRequestTime(), 'custom', 'H:i', $timezone)] + ), + ]; + } + + // This overrides BaseFieldDefinition. @todo: apply for form and widget. + $element['comment'] = [ + '#type' => 'textarea', + '#required' => $settings_comment == '2', + '#access' => $settings_comment != '0', // Align with action buttons. + '#title' => t('Workflow comment'), + '#description' => t('A comment to put in the workflow log.'), + '#default_value' => $transition ? $transition->getComment() : '', + '#rows' => 2, + ]; + + // In WorkflowTransitionForm, a default 'Submit' button is added over there. + // In Entity Form, a button per permitted state is added in workflow_form_alter(). + if ($settings_options_type == 'buttons' || $settings_options_type == 'dropbutton') { + // D7: How do action buttons work? See also d.o. issue #2187151. + // D7: Create 'action buttons' per state option. Set $sid property on each button. + // 1. Admin sets ['widget']['options']['#type'] = 'buttons'. + // 2. This function formElement() creates 'action buttons' per state option; + // sets $sid property on each button. + // 3. User clicks button. + // 4. Callback _workflow_transition_form_validate_buttons() sets proper State. + // 5. Callback _workflow_transition_form_validate_buttons() sets Submit function. + + // Performance: inform workflow_form_alter() to do its job. + _workflow_use_action_buttons($settings_options_type); + + // Make sure the '#type' is not set to the invalid 'buttons' value. + // It will be replaced by action buttons, but sometimes, the select box + // is still shown. + // @see workflow_form_alter(). + $element['to_sid']['#type'] = 'select'; + $element['to_sid']['#access'] = FALSE; + } + return $element; + } + + /** + * Returns a unique string identifying the form. + * + * @return string + */ + protected static function getFormId() { + return 'workflow_transition_form'; //@todo D8-port: add $form_id for widget and History tab. + } + + /** + * Implements ContentEntityForm::copyFormValuesToEntity(), and is called from: + * - WorkflowTransitionForm::copyFormValuesToEntity() + * - WorkflowDefaultWidget + * + * N.B. in contrary to ContentEntityForm::copyFormValuesToEntity(), + * - parameter 1 is returned as result, to be able to create a new Transition object. + * - parameter 3 is not $form_state (from Form), but an $item array (from Widget). + * + * @param \Drupal\Core\Entity\EntityInterface $transition + * @param array $form + * @param FormStateInterface $form_state + * @param array $item + * + * @return WorkflowTransitionInterface + */ + static public function copyFormValuesToTransition(EntityInterface $transition, array $form, FormStateInterface $form_state, array $item) { + /** @var WorkflowTransitionInterface $transition */ + /** @var \Drupal\user\UserInterface $user */ + $user = workflow_current_user(); // @todo #2287057: verify if submit() really is only used for UI. If not, $user must be passed. + + /** + * Derived input + */ + // Make sure we have subset ['workflow_scheduled_date_time'] + if (!isset($item['to_sid'])) { + $entity_id = $transition->getTargetEntityId(); + drupal_set_message(t('Error: content @id has no workflow attached. The data is not saved.', ['@id' => $entity_id]), 'error'); + // The new state is still the previous state. + return $transition; + } + + // In WorkflowTransitionForm, we receive the complete $form_state. + // Remember, the workflow_scheduled element is not set on 'add' page. + $scheduled = !empty($item['workflow_scheduling']['scheduled']); + $schedule_values = ($scheduled) ? $item['workflow_scheduling']['date_time'] : []; + + // Get user input from element. + $to_sid = $item['to_sid']; + $comment = $item['comment']; + $force = FALSE; + + // @todo D8: add the VBO use case. + /* + // Determine if the transition is forced. + // This can be set by a 'workflow_vbo action' in an additional form element. + $force = isset($form_state['input']['workflow_force']) ? $form_state['input']['workflow_force'] : FALSE; + if (!$entity) { + // E.g., on VBO form. + } + */ + + // @todo D8-port: add below exception. + // Extract the data from $items, depending on the type of widget. + // @todo D8: use MassageFormValues($item, $form, $form_state). + /* + $old_sid = workflow_node_previous_state($entity, $entity_type, $field_name); + if (!$old_sid) { + // At this moment, $old_sid should have a value. If the content does not + // have a state yet, old_sid contains '(creation)' state. But if the + // content is not associated to a workflow, old_sid is now 0. This may + // happen in workflow_vbo, if you assign a state to non-relevant nodes. + $entity_id = entity_id($entity_type, $entity); + drupal_set_message(t('Error: content @id has no workflow attached. The data is not saved.', array('@id' => $entity_id)), 'error'); + // The new state is still the previous state. + $new_sid = $old_sid; + return $new_sid; + } + */ + + $timestamp = \Drupal::time()->getRequestTime(); + if ($scheduled) { + // Fetch the (scheduled) timestamp to change the state. + // Override $timestamp. + $scheduled_date_time = implode(' ', [ + $schedule_values['workflow_scheduled_date'], + $schedule_values['workflow_scheduled_hour'], + // $schedule_values['workflow_scheduled_timezone'], + ]); + $timezone = $schedule_values['workflow_scheduled_timezone']; + $old_timezone = date_default_timezone_get(); + date_default_timezone_set($timezone); + $timestamp = strtotime($scheduled_date_time); + date_default_timezone_set($old_timezone); + if (!$timestamp) { + // Time should have been validated in form/widget. + $timestamp = \Drupal::time()->getRequestTime(); + } + } + + /** + * Process + */ + + /* + * Create a new ScheduledTransition. + */ + if ($scheduled) { + $transition_entity = $transition->getTargetEntity(); + $field_name = $transition->getFieldName(); + $from_sid = $transition->getFromSid(); + /** @var $transition WorkflowTransitionInterface */ + $transition = WorkflowScheduledTransition::create([$from_sid, 'field_name' => $field_name]); + $transition->setTargetEntity($transition_entity); + $transition->setValues($to_sid, $user->id(), $timestamp, $comment); + } + if (!$transition->isExecuted()) { + // Set new values. + // When editing an existing Transition, only comments may change. + $transition->setValues($to_sid, $user->id(), $timestamp, $comment); + $transition->schedule($scheduled); + $transition->force($force); + } + $transition->setComment($comment); + + // Determine and add the attached fields. + // Caveat: This works automatically on a Workflow Form, + // but only with a hack on a widget. + // @todo: Attached fields are not supported in ScheduledTransitions. + $fields = WorkflowManager::getAttachedFields('workflow_transition', $transition->bundle()); + /** @var \Drupal\Core\Field\Entity\BaseFieldOverride $nodeField */ + foreach ($fields as $field_name => $field) { + $user_input = isset($form_state->getUserInput()[$field_name]) ? $form_state->getUserInput()[$field_name] : []; + if (isset($item[$field_name])) { + // On Workflow Form (e.g., history tab, block). + // @todo, in latest tests, this line seems not necessary. + $transition->{$field_name} = $item[$field_name]; + } + elseif ($user_input) { + // On Workflow Widget (e.g., on node, comment). + // @todo, some field types are not supported here. + $transition->{$field_name} = $user_input; + } + } + + return $transition; + } + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return [ + '#input' => TRUE, + '#return_value' => 1, + '#process' => [ + [$class, 'processTransition'], + [$class, 'processAjaxForm'], + //array($class, 'processGroup'), + ], + '#element_validate' => [ + [$class, 'validateTransition'], + ], + '#pre_render' => [ + [$class, 'preRenderTransition'], + //array($class, 'preRenderGroup'), + ], + //'#theme' => 'input__checkbox', + //'#theme' => 'input__textfield', + '#theme_wrappers' => ['form_element'], + //'#title_display' => 'after', + ]; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/Workflow.php b/sites/all/modules/contrib/admin/workflow/src/Entity/Workflow.php new file mode 100644 index 000000000..701920feb --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/Workflow.php @@ -0,0 +1,444 @@ +getCreationState(); + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public static function postLoad(EntityStorageInterface $storage, array &$entities) { + /** @var Workflow $workflow */ + foreach($entities as &$workflow) { + //Better performance, together with Annotation static_cache = TRUE. + // Load the states, and set the creation state. + $workflow->getStates(); + $workflow->getCreationState(); + } + } + + /** + * Given a wid, delete the workflow and its data. + */ + public function delete() { + if (!$this->isDeletable()) { + // @todo: throw error if not workflow->isDeletable(). + } + else { + // Delete associated state (also deletes any associated transitions). + foreach ($this->getStates($all = TRUE) as $state) { + $state->deactivate(''); + $state->delete(); + } + + // Delete the workflow. + parent::delete(); + } + } + + /** + * {@inheritdoc} + */ + public function isValid() { + $is_valid = TRUE; + + // Don't allow Workflow without states. There should always be a creation state. + $states = $this->getStates($all = FALSE); + if (count($states) < 1) { + // That's all, so let's remind them to create some states. + $message = t('Workflow %workflow has no states defined, so it cannot be assigned to content yet.', + ['%workflow' => $this->label()]); + drupal_set_message($message, 'warning'); + + // Skip allowing this workflow. + $is_valid = FALSE; + } + + // Also check for transitions, at least out of the creation state. Don't filter for roles. + $transitions = $this->getTransitionsByStateId($this->getCreationSid(), ''); + if (count($transitions) < 1) { + // That's all, so let's remind them to create some transitions. + $message = t('Workflow %workflow has no transitions defined, so it cannot be assigned to content yet.', + ['%workflow' => $this->label()]); + drupal_set_message($message, 'warning'); + + // Skip allowing this workflow. + $is_valid = FALSE; + } + + return $is_valid; + } + + /** + * {@inheritdoc} + */ + public function isDeletable() { + + // May not be deleted if assigned to a Field. + foreach ($fields = _workflow_info_fields() as $field_info) { + if ($field_info->getSetting('workflow_type') == $this->id()) { + return FALSE; + } + } + return TRUE; + } + + /** + * Property functions. + */ + + /** + * {@inheritdoc} + */ + public function getWorkflowId() { + return $this->id(); + } + + /** + * {@inheritdoc} + */ + public function createState($sid, $save = TRUE) { + $wid = $this->id(); + /* @var $state WorkflowState */ + $state = WorkflowState::load($sid); + if (!$state || $wid != $state->getWorkflowId()) { + $state = WorkflowState::create($values = ['id' => $sid, 'wid' => $wid]); + if ($save) { + $state->save(); + } + } + + // Maintain the new object in the workflow. + $this->states[$state->id()] = $state; + + return $state; + } + + /** + * {@inheritdoc} + */ + public function getCreationState() { + // First, find it. + if (!$this->creation_state) { + foreach ($this->getStates($all = TRUE) as $state) { + if ($state->isCreationState()) { + $this->creation_state = $state; + $this->creation_sid = $state->id(); + } + } + } + + // First, then, create it. + if (!$this->creation_state) { + $state = $this->createState(WORKFLOW_CREATION_STATE_NAME); + $this->creation_state = $state; + $this->creation_sid = $state->id(); + } + + return $this->creation_state; + } + + /** + * {@inheritdoc} + */ + public function getCreationSid() { + if (!$this->creation_sid) { + $state = $this->getCreationState(); + return $state->id(); + } + return $this->creation_sid; + } + + /** + * {@inheritdoc} + */ + public function getFirstSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE) { + $creation_state = $this->getCreationState(); + $options = $creation_state->getOptions($entity, $field_name, $user, $force); + if ($options) { + $keys = array_keys($options); + $sid = $keys[0]; + } + else { + // This should never happen, but it did during testing. + drupal_set_message(t('There are no workflow states available. Please notify your site administrator.'), 'error'); + $sid = 0; + } + return $sid; + } + + /** + * {@inheritdoc} + */ + public function getNextSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE) { + $current_sid = WorkflowManager::getCurrentStateId($entity, $field_name); + /* @var $current_state WorkflowState */ + $current_state = WorkflowState::load($current_sid); + $options = $current_state->getOptions($entity, $field_name, $user, $force); + // Loop over every option. To find the next one. + $flag = $current_state->isCreationState(); + $new_sid = $current_state->id(); + + foreach ($options as $sid => $name) { + if ($flag) { + $new_sid = $sid; + break; + } + if ($sid == $current_state->id()) { + $flag = TRUE; + } + } + + return $new_sid; + } + + /** + * {@inheritdoc} + */ + public function getStates($all = FALSE, $reset = FALSE) { + $wid = $this->id(); + + if ($reset) { + $this->states = $wid ? WorkflowState::loadMultiple([], $wid, $reset) : []; + } + elseif ($this->states === NULL) { + $this->states = $wid ? WorkflowState::loadMultiple([], $wid, $reset) : []; + } + elseif ($this->states === []) { + $this->states = $wid ? WorkflowState::loadMultiple([], $wid, $reset) : []; + } + + // Do not unset, but add to array - you'll remove global objects otherwise. + $states = []; + foreach ($this->states as $state) { + $id = $state->id(); + if ($all === TRUE) { + $states[$id] = $state; + } + elseif (($all === FALSE) && ($state->isActive() && !$state->isCreationState())) { + $states[$id] = $state; + } + elseif (($all == 'CREATION') && ($state->isActive() || $state->isCreationState())) { + $states[$id] = $state; + } + else { + // Do not add state. + } + } + return $states; + } + + /** + * {@inheritdoc} + */ + public function getState($sid) { + $wid = $this->id(); + $state = WorkflowState::load($sid); + if (!$wid || $wid == $state->getWorkflowId()) { + return $state; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function createTransition($from_sid, $to_sid, $values = []) { + $config_transition = NULL; + + // First check if this transition already exists. + $transitions = $this->getTransitionsByStateId($from_sid, $to_sid); + if ($transitions) { + $config_transition = reset($transitions); + } + else { + $values['wid'] = $this->id(); + $values['from_sid'] = $from_sid; + $values['to_sid'] = $to_sid; + $config_transition = WorkflowConfigTransition::create($values); + $config_transition->save(); + } + // Maintain the new object in the workflow. + $this->transitions[$config_transition->id()] = $config_transition; + + return $config_transition; + } + + /** + * {@inheritdoc} + */ + public function sortTransitions() { + // Sort the transitions on state weight. + uasort($this->transitions, ['Drupal\workflow\Entity\WorkflowConfigTransition', 'sort'] ); + } + + /** + * {@inheritdoc} + */ + public function getTransitions(array $ids = NULL, array $conditions = []) { + $config_transitions = []; + + // Get filters on 'from' states, 'to' states, roles. + $from_sid = isset($conditions['from_sid']) ? $conditions['from_sid'] : FALSE; + $to_sid = isset($conditions['to_sid']) ? $conditions['to_sid'] : FALSE; + + // Get valid states + creation state. + $states = $this->getStates('CREATION'); + // Cache all transitions in the workflow. + if (!$this->transitions) { + $this->transitions = WorkflowConfigTransition::loadMultiple($ids); + + $this->sortTransitions(); + } + + /* @var $config_transition WorkflowConfigTransition */ + foreach ($this->transitions as &$config_transition) { + if (!isset($states[$config_transition->getFromSid()])) { + // Not a valid transition for this workflow. @todo: delete them. + } + elseif ($from_sid && $from_sid != $config_transition->getFromSid()) { + // Not the requested 'from' state. + } + elseif ($to_sid && $to_sid != $config_transition->getToSid()) { + // Not the requested 'to' state. + } + else { + // Transition is allowed, permitted. Add to list. + $config_transitions[$config_transition->id()] = $config_transition; + } + } + return $config_transitions; + } + + /** + * {@inheritdoc} + */ + public function getTransitionsById($tid) { + return $this->getTransitions([$tid]); + } + + /** + * {@inheritdoc} + */ + public function getTransitionsByStateId($from_sid, $to_sid) { + $conditions = [ + 'from_sid' => $from_sid, + 'to_sid' => $to_sid, + ]; + return $this->getTransitions(NULL, $conditions); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransition.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransition.php new file mode 100644 index 000000000..7e25f7fc2 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransition.php @@ -0,0 +1,222 @@ +from_sid = $from_sid; + $this->to_sid = $to_sid; + } + + /** + * {@inheritdoc} + */ + public function save() { + $workflow = $this->getWorkflow(); + + // To avoid double posting, check if this (new) transition already exist. + if (empty($this->id())) { + if ($workflow) { + $config_transitions = $workflow->getTransitionsByStateId($this->from_sid, $this->to_sid); + $config_transition = reset($config_transitions); + if ($config_transition) { + $this->set('id', $config_transition->id()); + } + } + } + + // Create the machine_name. This can be used to rebuild/revert the Feature in a target system. + if (empty($this->id())) { + $wid = $workflow->id(); + $this->set('id', implode('', [$wid, substr($this->from_sid, strlen($wid)), substr($this->to_sid, strlen($wid))])); + } + + $status = parent::save(); + + if ($status) { + // Save in current workflow for the remainder of this page request. + // Keep in sync with Workflow::getTransitions() ! + if ($workflow) { + $workflow->transitions[$this->id()] = $this; + // $workflow->sortTransitions(); + } + } + + return $status; + } + + /** @noinspection PhpMissingParentCallCommonInspection + * {@inheritdoc} + */ + public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) { + // Sort the entities using the entity class's sort() method. + // See \Drupal\Core\Config\Entity\ConfigEntityBase::sort(). + + /** @var WorkflowTransitionInterface $a */ + /** @var WorkflowTransitionInterface $b */ + if (!$a->getFromSid() || !$b->getFromSid()) { + return 0; + } + // First sort on From-State. + $from_state_a = $a->getFromState(); + $from_state_b = $b->getFromState(); + if ($from_state_a->weight < $from_state_b->weight) return -1; + if ($from_state_a->weight > $from_state_b->weight) return +1; + + // Then sort on To-State. + $to_state_a = $a->getToState(); + $to_state_b = $b->getToState(); + if ($to_state_a->weight < $to_state_b->weight) return -1; + if ($to_state_a->weight > $to_state_b->weight) return +1; + + return 0; + } + + /** + * Property functions. + */ + + /** + * {@inheritdoc} + */ + public function getWorkflow() { + if (!$this->workflow && $wid = $this->getWorkflowId()) { + $this->workflow = Workflow::load($wid); + } + return $this->workflow; + } + + /** + * {@inheritdoc} + */ + public function getWorkflowId() { + if (!$this->wid) { + $from_sid = $this->getFromSid(); + $to_sid = $this->getToSid(); + $state = WorkflowState::load($to_sid ? $to_sid : $from_sid); + $this->wid = $state->getWorkflowId(); + } + return $this->wid; + } + + /** + * {@inheritdoc} + */ + public function getFromState() { + return WorkflowState::load($this->from_sid); + } + + /** + * {@inheritdoc} + */ + public function getToState() { + return WorkflowState::load($this->to_sid); + } + + /** + * {@inheritdoc} + */ + public function getFromSid() { + return $this->from_sid; + } + + /** + * {@inheritdoc} + */ + public function getToSid() { + return $this->to_sid; + } + + /** + * {@inheritdoc} + */ + public function isAllowed(UserInterface $user, $force = FALSE) { + + $type_id = $this->getWorkflowId(); + if ($user->hasPermission("bypass $type_id workflow_transition access")) { + // Superuser is special. And $force allows Rules to cause transition. + return TRUE; + } + if ($force) { + return TRUE; + } + if ($this->getFromSid() == $this->getToSid()) { + // Anyone may save an entity without changing state. + return TRUE; + } + return TRUE == array_intersect($user->getRoles(), $this->roles); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransitionInterface.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransitionInterface.php new file mode 100644 index 000000000..b0a254600 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowConfigTransitionInterface.php @@ -0,0 +1,69 @@ +settingsForm() + * + * @return bool + * $is_valid + */ + public function isValid(); + + /** + * Returns if the Workflow may be deleted. + * + * @return bool + * TRUE if a Workflow may safely be deleted. + */ + public function isDeletable(); + + /** + * Create a new state for this workflow. + * + * @param string $sid + * @param bool $save + * Indicator if the new state must be saved. Normally, the new State is + * saved directly in the database. This is because you can use States only + * with Transitions, and they rely on State IDs which are generated + * magically when saving the State. But you may need a temporary state. + * @return \Drupal\workflow\Entity\WorkflowState + * The new state. + */ + public function createState($sid, $save = TRUE); + + /** + * Gets the initial state for a newly created entity. + */ + public function getCreationState(); + + /** + * Gets the ID of the initial state for a newly created entity. + */ + public function getCreationSid(); + + /** + * Gets the first valid state ID, after the creation state. + * + * Uses WorkflowState::getOptions(), because this does an access check. + * The first State ID is user-dependent! + * + * @param EntityInterface|null $entity + * The entity at hand. May be NULL (E.g., on a Field settings page). + * @param $field_name + * @param AccountInterface $user + * @param bool $force + * + * @return string + * A State ID. + */ + public function getFirstSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE); + + /** + * Returns the next state for the current state. + * Is used in VBO Bulk actions. + * + * @param EntityInterface $entity + * The entity at hand. + * @param $field_name + * @param AccountInterface $user + * @param bool $force + * + * @return string + * A State ID. + */ + public function getNextSid(EntityInterface $entity, $field_name, AccountInterface $user, $force = FALSE); + + /** + * Gets all states for a given workflow. + * + * @param mixed $all + * Indicates to which states to return. + * - TRUE = all, including Creation and Inactive; + * - FALSE = only Active states, not Creation; + * - 'CREATION' = only Active states, including Creation. + * @param bool $reset + * + * @return WorkflowState[] + * An array of WorkflowState objects. + */ + public function getStates($all = FALSE, $reset = FALSE); + + /** + * Gets a state for a given workflow. + * + * @param string $sid + * A state ID. + * + * @return WorkflowState + * A WorkflowState object. + */ + public function getState($sid); + + /** + * Creates a Transition for this workflow. + * + * @param string $from_sid + * @param string $to_sid + * @param array $values + * + * @return mixed|null|static + */ + public function createTransition($from_sid, $to_sid, $values = []); + + /** + * Sorts all Transitions for this workflow, according to State weight. + * + * This is only needed for the Admin UI. + */ + public function sortTransitions(); + + /** + * Loads all allowed ConfigTransitions for this workflow. + * + * @param array|null $ids + * Array of Transitions IDs. If NULL, show all transitions. + * @param array $conditions + * $conditions['from_sid'] : if provided, a 'from' State ID. + * $conditions['to_sid'] : if provided, a 'to' state ID. + * + * @return \Drupal\workflow\Entity\WorkflowConfigTransition[] + */ + public function getTransitions(array $ids = NULL, array $conditions = []); + + public function getTransitionsById($tid); + + /** + * Get a specific transition. + * + * @param string $from_sid + * @param string $to_sid + * + * @return WorkflowConfigTransition[] + */ + public function getTransitionsByStateId($from_sid, $to_sid); + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManager.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManager.php new file mode 100644 index 000000000..720c8f0a9 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManager.php @@ -0,0 +1,468 @@ +entityTypeManager = $entity_type_manager; + $this->queryFactory = $query_factory; + $this->userConfig = $config_factory->get('user.settings'); + $this->stringTranslation = $string_translation; + $this->moduleHandler = $module_handler; + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function executeScheduledTransitionsBetween($start = 0, $end = 0) { + $clear_cache = FALSE; + + // If the time now is greater than the time to execute a transition, do it. + foreach (WorkflowScheduledTransition::loadBetween($start, $end) as $scheduled_transition) { + $field_name = $scheduled_transition->getFieldName(); + $entity = $scheduled_transition->getTargetEntity(); + $from_sid = $scheduled_transition->getFromSid(); + + // Make sure transition is still valid: the entity must still be in + // the state it was in, when the transition was scheduled. + if (!$entity) { + continue; + } + + $current_sid = workflow_node_current_state($entity, $field_name); + if (!$current_sid || ($current_sid != $from_sid)) { + // Entity is not in the same state it was when the transition + // was scheduled. Defer to the entity's current state and + // abandon the scheduled transition. + $message = t('Scheduled Transition is discarded, since Entity has state ID %sid1, instead of expected ID %sid2.'); + $scheduled_transition->logError($message, 'error', $current_sid, $from_sid); + $scheduled_transition->delete(); + continue; + } + + // If user didn't give a comment, create one. + $comment = $scheduled_transition->getComment(); + if (empty($comment)) { + $scheduled_transition->addDefaultComment(); + } + + // Do transition. Force it because user who scheduled was checked. + // The scheduled transition is not scheduled anymore, and is also deleted from DB. + // A watchdog message is created with the result. + $scheduled_transition->schedule(FALSE); + $scheduled_transition->executeAndUpdateEntity(TRUE); + + if (!$field_name) { + $clear_cache = TRUE; + } + } + + if ($clear_cache) { + // Clear the cache so that if the transition resulted in a entity + // being published, the anonymous user can see it. + Cache::invalidateTags(['rendered']); + } + } + + /** + * {@inheritdoc} + */ + public static function executeTransitionsOfEntity(EntityInterface $entity) { + + // Avoid this hook on workflow objects. + if (in_array($entity->getEntityTypeId(), [ + 'workflow_type', + 'workflow_state', + 'workflow_config_transition', + 'workflow_transition', + 'workflow_scheduled_transition', + ])) { + return; + } + + $user = workflow_current_user(); + + foreach (_workflow_info_fields($entity) as $field_info) { + $field_name = $field_info->getName(); + + // Transition is created in widget or WorkflowTransitionForm. + /** @var $transition WorkflowTransitionInterface */ + $transition = $entity->$field_name->__get('workflow_transition'); + if (!$transition) { + // We come from creating/editing an entity via entity_form, with core widget or hidden Workflow widget. + // @todo D8: from an Edit form with hidden widget. + /** @noinspection PhpUndefinedFieldInspection */ + if ($entity->original) { + // Editing a Node with hidden Widget. State change not possible, so bail out. +// $entity->$field_name->value = $entity->original->$field_name->value; +// continue; + } + + // Creating a Node with hidden Workflow Widget. Generate valid first transition. + // @todo D8: Test Creating a non-Node Entity with hidden Workflow widget. + $comment = ''; + $old_sid = WorkflowManager::getPreviousStateId($entity, $field_name); + $new_sid = $entity->$field_name->value; + if ((!$new_sid) && $wid = $field_info->getSetting('workflow_type')) { + /** @var Workflow $workflow */ + $workflow = Workflow::load($wid); + $new_sid = $workflow->getFirstSid($entity, $field_name, $user); + } + $transition = WorkflowTransition::create([$old_sid, 'field_name' => $field_name]); + $transition->setValues($new_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment, TRUE); + } + + // We come from Content/Comment edit page, from widget. + // Set the just-saved entity explicitly. Not necessary for update, + // but upon insert, the old version didn't have an ID, yet. + $transition->setTargetEntity($entity); + + if ($transition->isScheduled()) { + $executed = $transition->save(); // Returns a positive integer. + } + elseif ($entity->getEntityTypeId() == 'comment') { + // If Transition is added via CommentForm, save Comment AND Entity. + // Execute and check the result. + $new_sid = $transition->executeAndUpdateEntity(); + $executed = ($new_sid == $transition->getToSid()) ? TRUE : FALSE; + } + else { + // Execute and check the result. + $new_sid = $transition->execute(); + $executed = ($new_sid == $transition->getToSid()) ? TRUE : FALSE; + } + + // If the transition failed, revert the entity workflow status. + // For new entities, we do nothing: it has no original. + if (!$executed && isset($entity->original)) { + $originalValue = $entity->original->{$field_name}->value; + $entity->{$field_name}->setValue($originalValue); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getPreviousStateId(EntityInterface $entity, $field_name = '') { + $sid = ''; + + if (!$entity) { + return $sid; + } + + // If $field_name is not known, yet, determine it. + $field_name = ($field_name) ? $field_name : workflow_get_field_name($entity, $field_name); + // If $field_name is found, get more details. + if (!$field_name) { + // Return the initial value. + return $sid; + } + + // A node may not have a Workflow attached. + if ($entity->isNew()) { + // A new Node. D7: $is_new is not set when saving terms, etc. + $sid = self::getCreationStateId($entity, $field_name); + } + else { + // @todo?: Read the history with an explicit langcode. + // @todo D8: #2373383 add integration with older revisions via Revisioning module. + $langcode = ''; // $entity->language()->getId(); + $entity_type = $entity->getEntityTypeId(); + $last_transition = WorkflowTransition::loadByProperties($entity_type, $entity->id(), [], $field_name, $langcode, 'DESC'); + if ($last_transition) { + $sid = $last_transition->getToSid(); // @see #2637092, #2612702 + } + } + + if (!$sid) { + // No history found on an existing entity. + $sid = self::getCreationStateId($entity, $field_name); + } + + return $sid; + } + + /** + * Gets the creation sid for a given $entity and $field_name. + * + * Is a helper function for: + * - workflow_node_current_state() + * - workflow_node_previous_state() + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param string $field_name + * + * @return string + * The ID of the creation State for the Workflow of the field. + */ + private static function getCreationStateId($entity, $field_name) { + $sid = ''; + + /** @var \Drupal\Core\Config\Entity\ConfigEntityBase $entity */ + /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_config */ + $field_config = $entity->get($field_name)->getFieldDefinition(); + $field_storage = $field_config->getFieldStorageDefinition(); + $wid = $field_storage->getSetting('workflow_type'); + if ($wid) { + /** @var Workflow $workflow */ + $workflow = Workflow::load($wid); + if (!$workflow) { + drupal_set_message(t('Workflow %wid cannot be loaded. Contact your system administrator.', ['%wid' => $wid]), 'error'); + } + else { + $sid = $workflow->getCreationSid(); + } + } + + return $sid; + } + + /** + * {@inheritdoc} + */ + public static function deleteUser(AccountInterface $account) { + self::cancelUser([], $account, 'user_cancel_delete'); + } + + /** + * {@inheritdoc} + */ + public static function cancelUser($edit, AccountInterface $account, $method) { + + switch ($method) { + case 'user_cancel_block': // Disable the account and keep its content. + case 'user_cancel_block_unpublish': // Disable the account and unpublish its content. + // Do nothing. + break; + case 'user_cancel_reassign': // Delete the account and make its content belong to the Anonymous user. + case 'user_cancel_delete': // Delete the account and its content. + + // Update tables for deleted account, move account to user 0 (anon.) + // ALERT: This may cause previously non-Anonymous posts to suddenly + // be accessible to Anonymous. + + /** + * Given a user id, re-assign history to the new user account. Called by user_delete(). + */ + $uid = $account->id(); + $new_uid = 0; + + db_update('workflow_transition_history') + ->fields(['uid' => $new_uid]) + ->condition('uid', $uid, '=') + ->execute(); + db_update('workflow_transition_schedule') + ->fields(['uid' => $new_uid]) + ->condition('uid', $uid, '=') + ->execute(); + + break; + } + } + + /** + * {@inheritdoc} + */ + public static function participateUserRoles(EntityInterface $workflow) { + $type_id = $workflow->id(); + foreach (user_roles() as $rid => $role) { + $perms = ["create $type_id workflow_transition" => 1]; + user_role_change_permissions($rid, $perms); // <=== Enable Roles. + } + } + + /** + * {@inheritdoc} + */ + public static function getWorkflowTransitionForm(EntityInterface $entity, string $field_name) { + // Create a transition, to pass to the form. No need to use setValues(). + $current_sid = workflow_node_current_state($entity, $field_name); + $transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]); + $transition->setTargetEntity($entity); + // Create the WorkflowTransitionForm. + /** @var \Drupal\Core\Entity\EntityFormBuilder $entity_form_builder */ + $entity_form_builder = \Drupal::getContainer()->get('entity.form_builder'); + $form = $entity_form_builder->getForm($transition, 'add'); + return $form; + } + + /** + * {@inheritdoc} + */ + public function getFields($entity_type_id) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) { + return []; + } + + $map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('workflow'); + return isset($map[$entity_type_id]) ? $map[$entity_type_id] : []; + } + + /** + * {@inheritdoc} + */ + public static function getAttachedFields($entity_type_id, $bundle) { + $added_fields = []; + + // Determine the fields added by Field UI. + $entity_field_manager = \Drupal::service('entity_field.manager'); + //$extra_fields = $this->entityFieldManager->getExtraFields($entity_type_id, $bundle); + //$base_fields = $entity_field_manager->getBaseFieldDefinitions($entity_type_id, $bundle); + $fields = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle); + foreach ($fields as $key => $field) { + // Remove BaseFieldDefinition, BaseFieldOverride. + if (($field instanceof FieldConfig)) { + $added_fields[$key] = $field; + } + } + return $added_fields; + } + + /** + * {@inheritdoc} + */ + public static function getCurrentStateId(EntityInterface $entity, $field_name = '') { + $sid = ''; + + if (!$entity) { + return $sid; + } + + // If $field_name is not known, yet, determine it. + $field_name = ($field_name) ? $field_name : workflow_get_field_name($entity, $field_name); + // If $field_name is found, get more details. + if (!$field_name || !isset($entity->$field_name)) { + // Return the initial value. + return $sid; + } + + // Normal situation: get the value. + $sid = $entity->$field_name->value; + + // Entity is new or in preview or there is no current state. Use previous state. + // (E.g., content was created before adding workflow.) + if ( !$sid || !empty($entity->isNew()) || !empty($entity->in_preview) ) { + $sid = self::getPreviousStateId($entity, $field_name); + } + + return $sid; + } + + /** + * {@inheritdoc} + */ + public static function isOwner(AccountInterface $account, EntityInterface $entity = NULL) { + $is_owner = FALSE; + + // @todo: Keep below code aligned between WorkflowState, ~Transition, ~TransitionListController + // Determine if user is owner of the entity. + $uid = ($account) ? $account->id() : -1; + // Get the entity's ID and Author ID. + $entity_id = ($entity) ? $entity->id() : ''; + // Some entities (e.g., taxonomy_term) do not have a uid. + // $entity_uid = $entity->get('uid'); // isset($entity->uid) ? $entity->uid : 0; + $entity_uid = (method_exists($entity, 'getOwnerId')) ? $entity->getOwnerId() : -1; + + if (!$entity_id) { + // This is a new entity. User is author. Add 'author' role to user. + $is_owner = TRUE; + } + elseif (($entity_uid > 0) && ($uid > 0) && ($entity_uid == $uid)) { + // This is an existing entity. User is author. + // D8: use "access own" permission. D7: Add 'author' role to user. + // N.B.: If 'anonymous' is the author, don't allow access to History Tab, + // since anyone can access it, and it will be published in Search engines. + $is_owner = TRUE; + } + else { + // This is an existing entity. User is not the author. Do nothing. + } + return $is_owner; + } + + /** + * {@inheritdoc} + */ + public static function isWorkflowEntityType(string $entity_type_id) { + return in_array($entity_type_id, [ + 'workflow_type', + 'workflow_state', + 'workflow_config_transition', + 'workflow_transition', + 'workflow_scheduled_transition', + ]); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManagerInterface.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManagerInterface.php new file mode 100644 index 000000000..a39f62e7b --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowManagerInterface.php @@ -0,0 +1,173 @@ +is_scheduled = TRUE; + // This transition is not executed. + $this->is_executed = FALSE; + } + + /** + * {@inheritdoc} + */ + public function setValues($to_sid, $uid = NULL, $scheduled = NULL, $comment = '', $force_create = FALSE) { + parent::setValues($to_sid, $uid, $scheduled, $comment, $force_create); + } + + /** + * {@inheritdoc} + * + * This is a hack to avoid the following error, because ScheduledTransition is not a bundle of Workflow: + * Drupal\Component\Plugin\Exception\PluginNotFoundException: The "entity:workflow_scheduled_transition:eerste" plugin does not exist. in Drupal\Core\Plugin\DefaultPluginManager->doGetDefinition() (line 60 of core\lib\Drupal\Component\Plugin\Discovery\DiscoveryTrait.php). + */ + function validate() { + // Since this function generates an error in one use case (using WorkflowTransitionForm) + // and is not called in the other use case (using the Workflow Widget), + // this function is disabled for now. + // @todo: this function is only called in the WorkflowTransitionForm, not in the Widget. + // @todo: repair https://www.drupal.org/node/2896650 + + // The following is from return parent::validate(); + $this->validated = TRUE; + //$violations = $this->getTypedData()->validate(); + // return new EntityConstraintViolationList($this, iterator_to_array($violations)); + $violations = []; + return new EntityConstraintViolationList($this, $violations); + } + + /** + * CRUD functions. + */ + + /** + * {@inheritdoc} + * + * Saves a scheduled transition. If the transition is executed, save in history. + */ + public function save() { + + // If executed, save in history. + if ($this->is_executed) { + // Be careful, we are not a WorkflowScheduleTransition anymore! + // No fuzzing around, just copy the ScheduledTransition to a normal one. + $current_sid = $this->getFromSid(); + $field_name = $this->getFieldName(); + $executed_transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]); + $executed_transition->setTargetEntity($this->getTargetEntity()); + $executed_transition->setValues($this->getToSid(), $this->getOwnerId(), \Drupal::time()->getRequestTime(), $this->getComment()); + return $executed_transition->save(); // <-- exit !! + } + + $hid = $this->id(); + if (!$hid) { + // Insert the transition. Make sure it hasn't already been inserted. + // @todo: Allow a scheduled transition per revision. + $entity = $this->getTargetEntity(); + $found_transition = self::loadByProperties($entity->getEntityTypeId(), $entity->id(), [], $this->getFieldName(), $this->getLangcode()); + if ($found_transition) { + // Avoid duplicate entries. + $found_transition->delete(); + $result = parent::save(); + } + else { + $result = parent::save(); + } + } + else { + workflow_debug(__FILE__, __FUNCTION__, __LINE__ ); // @todo D8-port: still test this snippet. + // Update the transition. + $result = parent::save(); + } + + // Create user message. + if ($state = $this->getToState()) { + $entity = $this->getTargetEntity(); + $message = '%entity_title scheduled for state change to %state_name on %scheduled_date'; + $args = [ + '%entity_title' => $entity->label(), + '%state_name' => $state->label(), + '%scheduled_date' => $this->getTimestampFormatted(), + 'link' => ($this->getTargetEntityId() && $this->getTargetEntity()->hasLinkTemplate('canonical')) ? $this->getTargetEntity()->toLink(t('View'))->toString() : '', + ]; + \Drupal::logger('workflow')->notice($message, $args); + drupal_set_message(t($message, $args)); + } + + return $result; + } + + /** + * {@inheritdoc} + */ +// public static function loadMultiple(array $ids = NULL) { +// return parent::loadMultiple($ids); +// } + + /** + * {@inheritdoc} + */ + public static function loadByProperties($entity_type, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = 'workflow_scheduled_transition') { + // N.B. $transition_type is set as parameter default. + return parent::loadByProperties($entity_type, $entity_id, $revision_ids, $field_name, $langcode, $sort, $transition_type); + } + + /** + * {@inheritdoc} + */ + public static function loadMultipleByProperties($entity_type, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = 'workflow_scheduled_transition') { + // N.B. $transition_type is set as parameter default. + return parent::loadMultipleByProperties($entity_type, $entity_ids, $revision_ids, $field_name, $langcode, $limit, $sort, $transition_type); + } + + /** + * Given a time frame, get all scheduled transitions. + * + * @param int $start + * @param int $end + * + * @return WorkflowScheduledTransition[] + * An array of transitions. + */ + public static function loadBetween($start = 0, $end = 0) { + $transition_type = 'workflow_scheduled_transition'; // @todo: get this from annotation. + + /* @var $query \Drupal\Core\Entity\Query\QueryInterface */ + $query = \Drupal::entityQuery($transition_type) + ->sort('timestamp', 'ASC') + ->addTag($transition_type); + if ($start) { + $query->condition('timestamp', $start, '>'); + } + if ($end) { + $query->condition('timestamp', $end, '<'); + } + + $ids = $query->execute(); + $transitions = self::loadMultiple($ids); + return $transitions; + } + + + /** + * Property functions. + */ + + /** + * If a scheduled transition has no comment, a default comment is added before executing it. + */ + public function addDefaultComment() { + $this->setComment(t('Scheduled by user @uid.', ['@uid' => $this->getOwnerId()])); + } + + /** + * Define the fields. Modify the parent fields. + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = []; + + // Add the specific ID-field : tid vs. hid. + $fields['tid'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Transition ID')) + ->setDescription(t('The transition ID.')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + + // Get the rest of the fields. + $fields += parent::baseFieldDefinitions($entity_type); + + // The timestamp has a different description. + $fields['timestamp'] = []; // Reset old value. + $fields['timestamp'] = BaseFieldDefinition::create('created') + ->setLabel(t('Scheduled')) + ->setDescription(t('The date+time this transition is scheduled for.')) + ->setQueryable(FALSE) +// ->setTranslatable(TRUE) +// ->setDisplayOptions('view', array( +// 'label' => 'hidden', +// 'type' => 'timestamp', +// 'weight' => 0, +// )) +// ->setDisplayOptions('form', array( +// 'type' => 'datetime_timestamp', +// 'weight' => 10, +// )) +// ->setDisplayConfigurable('form', TRUE); + ->setRevisionable(TRUE); + + + // Remove the specific ID-field : tid vs. hid. + unset($fields['hid']); + + return $fields; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowState.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowState.php new file mode 100644 index 000000000..682736e5a --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowState.php @@ -0,0 +1,571 @@ +id(); + $wid = $this->wid; + $label = $this->label(); + + // Set the workflow-including machine_name. + if ($sid == WORKFLOW_CREATION_STATE_NAME) { + // Set the initial sid. + $sid = implode('_', [$wid, $sid]); + $this->set('id', $sid); + } + elseif (empty($sid)) { + if ($label) { + $transliteration = \Drupal::service('transliteration'); + $value = $transliteration->transliterate($label, LanguageInterface::LANGCODE_DEFAULT, '_'); + $value = strtolower($value); + $value = preg_replace('/[^a-z0-9_]+/', '_', $value); + $sid = implode('_', [$wid, $value]); + } + else { + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + $sid = 'state_' . $this->id(); + $sid = implode('_', [$wid, $sid]); + } + $this->set('id', $sid); + } + + return parent::save(); + } + + /** + * Get all states in the system, with options to filter, only where a workflow exists. + * + * @_deprecated WorkflowState::getStates() ==> WorkflowState::loadMultiple() + * + * {@inheritdoc} + * + * @param $wid + * The requested Workflow ID. + * @param bool $reset + * An option to refresh all caches. + * + * @return WorkflowState[] + * An array of cached states, keyed by state_id. + */ + public static function loadMultiple(array $ids = NULL, $wid = '', $reset = FALSE) { + $states = parent::loadMultiple(); + usort($states, ['Drupal\workflow\Entity\WorkflowState', 'sort'] ); + + // Filter onl Wid, if requested, E.g., by Workflow->getStates(). + // Set the ID as array key. + $result = []; + foreach ($states as $state) { + /** @var WorkflowState $state */ + if ((!$wid) || ($wid == $state->wid)) { + $result[$state->id()] = $state; + } + } + return $result; + } + + /** + * {@inheritdoc} + */ + public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) { + /** @var WorkflowState $a */ + /** @var WorkflowState $b */ + $a_wid = $a->wid; + $b_wid = $b->wid; + if ($a_wid == $b_wid) { + $a_weight = $a->getWeight(); + $b_weight = $b->getWeight(); + return ($a_weight < $b_weight) ? -1 : 1; + } + return ($a_wid < $b_wid) ? -1 : 1; + } + + /** + * Deactivate a Workflow State, moving existing content to a given State. + * + * @param int $new_sid + * The state ID, to which all affected entities must be moved. + */ + public function deactivate($new_sid) { + + $current_sid = $this->id(); + $force = TRUE; + + // Notify interested modules. We notify first to allow access to data before we zap it. + // - re-parents any entity that we don't want to orphan, whilst deactivating a State. + // - delete any lingering entity to state values. + // \Drupal::moduleHandler()->invokeAll('workflow', ['state delete', $current_sid, $new_sid, NULL, $force]); + // Invoke the hook. + \Drupal::moduleHandler()->invokeAll('entity_' . $this->getEntityTypeId() . '_predelete', [$this, $current_sid, $new_sid]); + + // Re-parent any entity that we don't want to orphan, whilst deactivating a State. + // TODO D8-port: State should not know about Transition: move this to Workflow->DeactivateState. + if ($new_sid) { + // A candidate for the batch API. + // @TODO: Future updates should seriously consider setting this with batch. + + $user = \Drupal::currentUser(); // We can use global, since deactivate() is a UI-only function. + $comment = t('Previous state deleted'); + + foreach (_workflow_info_fields() as $field_info) { + $entity_type_id = $field_info->getTargetEntityTypeId(); + $field_name = $field_info->getName(); // @todo: overwrites key? + + $result = []; + // CommentForm's are not re-parented upon Deactivate WorkflowState. + if ($entity_type_id != 'comment') { + $query = \Drupal::entityQuery($entity_type_id); + $query->condition($field_name, $current_sid, '='); + $result = $query->execute(); + } + + foreach ($result as $entity_id) { + $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id); + $transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]); + $transition->setTargetEntity($entity); + $transition->setValues($new_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment, TRUE); + $transition->force($force); + + // Execute Transition, invoke 'pre' and 'post' events, save new state in Field-table, save also in workflow_transition_history. + // For Workflow Node, only {workflow_node} and {workflow_transition_history} are updated. For Field, also the Entity itself. + // Execute transition and update the attached entity. + $new_sid = $transition->executeAndUpdateEntity($force); + } + } + } + + // Delete the transitions this state is involved in. + $workflow = Workflow::load($this->wid); + /** @var WorkflowInterface $workflow */ + /** @var WorkflowTransitionInterface $transition */ + foreach ($workflow->getTransitionsByStateId($current_sid, '') as $transition) { + $transition->delete(); + } + foreach ($workflow->getTransitionsByStateId('', $current_sid) as $transition) { + $transition->delete(); + } + + // Delete the state. -- We don't actually delete, just deactivate. + // This is a matter up for some debate, to delete or not to delete, since this + // causes name conflicts for states. In the meantime, we just stick with what we know. + // If you really want to delete the states, use workflow_cleanup module, or delete(). + $this->status = FALSE; + $this->save(); + + // Clear the cache. + self::loadMultiple([], 0, TRUE); + } + + /** + * Property functions. + */ + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->weight; + } + + /** + * Returns the Workflow ID of this State. + * + * @return string + * Workflow Id. + */ + public function getWorkflowId() { + return $this->wid; + } + + /** + * Returns the Workflow object of this State. + * + * @return Workflow + * Workflow object. + */ + public function getWorkflow() { + if (!isset($this->workflow)) { + $this->workflow = Workflow::load($this->wid); + } + return $this->workflow; + } + + /** + * @param Workflow $workflow + */ + public function setWorkflow(Workflow $workflow) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + + $this->wid = $workflow->id(); + $this->workflow = $workflow; + } + + /** + * Returns the Workflow object of this State. + * + * @return bool + * TRUE if state is active, else FALSE. + */ + public function isActive() { + return (bool) $this->status; + } + + /** + * Checks if the given state is the 'Create' state. + * + * @return bool + */ + public function isCreationState() { + return $this->sysid == WORKFLOW_CREATION_STATE; + } + + /** + * Determines if the Workflow Form must be shown. + * + * If not, a formatter must be shown, since there are no valid options. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param string $field_name + * @param \Drupal\Core\Session\AccountInterface $account + * @param bool $force + * + * @return bool + * TRUE = a form (a.k.a. widget) must be shown; FALSE = no form, a formatter must be shown instead. + */ + public function showWidget(EntityInterface $entity, $field_name, AccountInterface $account, $force) { + $options = $this->getOptions($entity, $field_name, $account, $force); + $count = count($options); + // The easiest case first: more then one option: always show form. + if ($count > 1) { + return TRUE; + } + // #2226451: Even in Creation state, we must have 2 visible states to show the widget. + // // Only when in creation phase, one option is sufficient, + // // since the '(creation)' option is not included in $options. + // // When in creation state, + // if ($this->isCreationState()) { + // return TRUE; + // } + return FALSE; + } + + /** + * Returns the allowed transitions for the current state. + * + * @param \Drupal\Core\Entity\EntityInterface|null $entity + * The entity at hand. May be NULL (E.g., on a Field settings page). + * @param string $field_name + * @param \Drupal\Core\Session\AccountInterface|null $account + * @param bool|false $force + * + * @return \Drupal\workflow\Entity\WorkflowConfigTransition[] + * An array of id=>transition pairs with allowed transitions for State. + */ + public function getTransitions(EntityInterface $entity = NULL, $field_name = '', AccountInterface $account = NULL, $force = FALSE) { + $transitions = []; + + if (!$workflow = $this->getWorkflow()) { + // No workflow, no options ;-) + return $transitions; + } + + // @todo: Keep below code aligned between WorkflowState, ~Transition, ~TransitionListController + + /** + * Get permissions of user, adding a Role to user, depending on situation. + */ + // Load a User object, since we cannot add Roles to AccountInterface. + /** @var \Drupal\user\UserInterface $user */ + $user = workflow_current_user($account); + // Determine if user is owner of the entity. + $is_owner = WorkflowManager::isOwner($user, $entity); + + // Check allow-ability of state change if user is not superuser (might be cron) + $type_id = $this->getWorkflowId(); + if ($user->hasPermission("bypass $type_id workflow_transition access")) { + // Superuser is special. And $force allows Rules to cause transition. + $force = TRUE; + } + + if ($is_owner) { + $user->addRole(WORKFLOW_ROLE_AUTHOR_RID); + } + + /** + * Get the object and its permissions. + */ + /** @var WorkflowConfigTransition[] $transitions */ + $transitions = $workflow->getTransitionsByStateId($this->id(), ''); + + /** + * Determine if user has Access. + */ + // Use default module permissions. + foreach ($transitions as $key => $transition) { + if (!$transition->isAllowed($user, $force)) { + unset($transitions[$key]); + } + } + // Let custom code add/remove/alter the available transitions, + // using the new drupal_alter. + // Modules may veto a choice by removing a transition from the list. + // Lots of data can be fetched via the $transition object. + $context = [ + 'entity' => $entity, // ConfigEntities do not have entity attached + 'field_name' => $field_name, // or field. + 'user' => $user, // user may have the custom role AUTHOR. + 'workflow' => $workflow, + 'state' => $this, + 'force' => $force, + ]; + \Drupal::moduleHandler()->alter('workflow_permitted_state_transitions', $transitions, $context); + + /** + * Determine if user has Access. + */ + // As of 8.x-1.x, below hook() is removed, in favour of above alter(). + // Let custom code change the options, using old_style hook. + // Above drupal_alter() calls hook_workflow_permitted_state_transitions_alter() only once. +// foreach ($transitions as $transition) { +// $to_sid = $transition->to_sid; +// $permitted = []; +// +// // We now have a list of config_transitions. Check each against the Entity. +// // Invoke a callback indicating that we are collecting state choices. +// // Modules may veto a choice by returning FALSE. +// // In this case, the choice is never presented to the user. +// if (!$force) { +// // @todo: D8-port: simplify interface for workflow_hook. Remove redundant context. +// $permitted = \Drupal::moduleHandler()->invokeAll('workflow', ['transition permitted', $transition, $user]); +// } +// +// // If vetoed by a module, remove from list. +// if (in_array(FALSE, $permitted, TRUE)) { +// unset($transitions[$transition->id()]); +// } +// } + + return $transitions; + } + + /** + * Returns the allowed values for the current state. + * + * @param object $entity + * The entity at hand. May be NULL (E.g., on a Field settings page). + * @param string $field_name + * @param \Drupal\Core\Session\AccountInterface|null $account + * @param bool $force + * + * @return array + * An array of sid=>label pairs. + * If $this->id() is set, returns the allowed transitions from this state. + * If $this->id() is 0 or FALSE, then labels of ALL states of the State's + * Workflow are returned. + */ + public function getOptions($entity, $field_name, AccountInterface $account = NULL, $force = FALSE) { + $options = []; + + // Define an Entity-specific cache per page load. + static $cache = []; + + $entity_id = ($entity) ? $entity->id() : ''; + $entity_type_id = ($entity) ? $entity->getEntityTypeId() : ''; + $current_sid = $this->id(); + + // Get options from page cache, using a non-empty index (just to be sure). + $entity_index = (!$entity) ? 'x' : $entity_id; + if (isset($cache[$entity_type_id][$entity_index][$force][$current_sid])) { + $options = $cache[$entity_type_id][$entity_index][$force][$current_sid]; + return $options; + } + + $workflow = $this->getWorkflow(); + if (!$workflow) { + // No workflow, no options ;-) + $options = []; + } + elseif (!$current_sid) { + // If no State ID is given, we return all states. + // We cannot use getTransitions, since there are no ConfigTransitions + // from State with ID 0, and we do not want to repeat States. + /** @var WorkflowState $state */ + foreach ($workflow->getStates() as $state) { + $options[$state->id()] = html_entity_decode(t('@label', ['@label' => $state->label()])); + } + } + else { + $transitions = $this->getTransitions($entity, $field_name, $account, $force); + foreach ($transitions as $transition) { + // Get the label of the transition, and if empty of the target state. + // Beware: the target state may not exist, since it can be invented + // by custom code in the above drupal_alter() hook. + if (!$label = $transition->label()) { + $to_state = $transition->getToState(); + $label = $to_state ? $to_state->label() : ''; + } + $to_sid = $transition->to_sid; + $options[$to_sid] = html_entity_decode(t('@label', ['@label' => $label])); + } + + // Save to entity-specific cache. + $cache[$entity_type_id][$entity_index][$force][$current_sid] = $options; + } + + return $options; + } + + /** + * Returns the number of entities with this state. + * + * @return int + * Counted number. + * + * @todo: add $options to select on entity type, etc. + */ + public function count() { + $count = 0; + $sid = $this->id(); + + foreach ($fields = _workflow_info_fields() as $field_info) { + $field_name = $field_info->getName(); + $query = \Drupal::entityQuery($field_info->getTargetEntityTypeId()); + // @see #2285983 for using SQLite on D7. + $count += $query + ->condition($field_name, $sid, '=') + ->count() // We only need the count. + ->execute(); + } + + return $count; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowStorage.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowStorage.php new file mode 100644 index 000000000..9cbd54a3f --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowStorage.php @@ -0,0 +1,107 @@ +stateService = $state_service; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('config.factory'), + $container->get('uuid'), + $container->get('language_manager'), + $container->get('state') + ); + } + + /** + * Returns a list of event names that are used by active workflows. + * + * @return string[] + * The list of event names keyed by event name. + */ + protected function getRegisteredEvents() { +// $events = []; +// foreach ($this->loadMultiple() as $rules_config) { +// $event = $rules_config->getEvent(); +// if ($event && !isset($events[$event])) { +// $events[$event] = $event; +// } +// } +// return $events; + } + + /** + * {@inheritdoc} + */ + public function doLoadMultiple(array $ids = NULL) { + $return = parent::doLoadMultiple($ids); + + return $return; + } + + /** + * {@inheritdoc} + */ + public function save(EntityInterface $entity) { + $return = parent::save($entity); + +// // Update the state of registered events. +// // @todo Should we trigger a container rebuild here as well? Might be a bit +// // expensive on every save? +// $this->stateService->set('rules.registered_events', $this->getRegisteredEvents()); + + return $return; + } + + /** + * {@inheritdoc} + */ + public function delete(array $entities) { + $return = parent::delete($entities); + return $return; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransition.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransition.php new file mode 100644 index 000000000..9e5f7990a --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransition.php @@ -0,0 +1,1085 @@ +getWorkflow() to fetch this. + protected $entity = NULL; // Use WorkflowTransition->getTargetEntity() to fetch this. + protected $user = NULL; // Use WorkflowTransition->getOwner() to fetch this. + + /* + * Extra data: describe the state of the transition. + */ + protected $is_scheduled; + protected $is_executed; + protected $is_forced = FALSE; + + /** + * Entity class functions. + */ + + /** + * Creates a new entity. + * + * @param array $values + * @param string $entityType + * The entity type of this Entity subclass. + * + * @param bool $bundle + * @param array $translations + * @internal param string $entity_type The entity type of the attached $entity. + * @see entity_create() + * + * No arguments passed, when loading from DB. + * All arguments must be passed, when creating an object programmatically. + * One argument $entity may be passed, only to directly call delete() afterwards. + */ + public function __construct(array $values = [], $entityType = 'workflow_transition', $bundle = FALSE, $translations = []) { + // Please be aware that $entity_type and $entityType are different things! + parent::__construct($values, $entityType, $bundle, $translations); + // This transition is not scheduled. + $this->is_scheduled = FALSE; + // This transition is not executed, if it has no hid, yet, upon load. + $this->is_executed = ($this->id() > 0); + } + + /** + * {@inheritdoc} + * + * @param array $values + * $values[0] may contain a Workflow object or State object or State ID. + * + * @return static + * The entity object. + */ + public static function create(array $values = []) { + if (is_array($values) && isset($values[0])) { + $value = $values[0]; + $values['wid'] = ''; + $values['from_sid'] = ''; + if (is_string($value) && $state = WorkflowState::load($value)) { + $values['wid'] = $state->getWorkflowId(); + $values['from_sid'] = $state->id(); + } + elseif (is_object($value) && $value instanceof WorkflowState) { + $state = $value; + $values['wid'] = $state->getWorkflowId(); + $values['from_sid'] = $state->id(); + } + } + + // Add default values. + $values += [ + 'timestamp' => \Drupal::time()->getRequestTime(), + 'uid' => \Drupal::currentUser()->id(), + ]; + return parent::create($values); + } + + /** + * {@inheritdoc} + */ + public function setValues($to_sid, $uid = NULL, $timestamp = NULL, $comment = '', $force_create = FALSE) { + // Normally, the values are passed in an array, and set in parent::__construct, but we do it ourselves. + + $uid = ($uid === NULL) ? workflow_current_user()->id() : $uid; + $from_sid = $this->getFromSid(); + + $this->set('to_sid', $to_sid); + $this->setOwnerId($uid); + $this->setTimestamp($timestamp == NULL ? \Drupal::time()->getRequestTime() : $timestamp); + $this->setComment($comment); + + // If constructor is called with new() and arguments. + if (!$from_sid && !$to_sid && !$this->getTargetEntity()) { + // If constructor is called without arguments, e.g., loading from db. + } + elseif ($from_sid && $this->getTargetEntity()) { + // Caveat: upon entity_delete, $to_sid is '0'. + // If constructor is called with new() and arguments. + } + elseif (!$from_sid) { + // Not all parameters are passed programmatically. + if (!$force_create) { + drupal_set_message( + t('Wrong call to constructor Workflow*Transition(%from_sid to %to_sid)', + ['%from_sid' => $from_sid, '%to_sid' => $to_sid]), + 'error'); + } + } + + return $this; + } + + /** + * CRUD functions. + */ + + /** + * Saves the entity. + * Mostly, you'd better use WorkflowTransitionInterface::execute(); + * + * {@inheritdoc} + */ + public function save() { + // return parent::save(); + + // Avoid custom actions for subclass WorkflowScheduledTransition. + if ($this->isScheduled()) { + return parent::save(); + } + if ($this->getEntityTypeId() != 'workflow_transition') { + return parent::save(); + } + + $transition = $this; + $entity_type = $transition->getTargetEntityTypeId(); + $entity_id = $transition->getTargetEntityId(); + $field_name = $transition->getFieldName(); + + // Remove any scheduled state transitions. + foreach (WorkflowScheduledTransition::loadMultipleByProperties($entity_type, [$entity_id], [], $field_name) as $scheduled_transition) { + /** @var WorkflowTransitionInterface $scheduled_transition */ + $scheduled_transition->delete(); + } + + // Check for no transition. + if ($this->isEmpty()) { + return SAVED_UPDATED; + } + + // Update the transition, if already exists. + if ($this->id()) { + return parent::save(); + } + + // Insert the transition. Make sure it hasn't already been inserted. + // @todo: Allow a scheduled transition per revision. + // @todo: Allow a state per language version (langcode). + $found_transition = self::loadByProperties($entity_type, $entity_id, [], $field_name); + if ($found_transition && + $found_transition->getTimestamp() == \Drupal::time()->getRequestTime() && + $found_transition->getToSid() == $this->getToSid()) { + return SAVED_UPDATED; + } + else { + return parent::save(); + } + + } + + /** + * {@inheritdoc} + */ +// public static function loadMultiple(array $ids = NULL) { +// return parent::loadMultiple($ids); +// } + + /** + * {@inheritdoc} + */ + public static function loadByProperties($entity_type, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = 'workflow_transition') { + $limit = 1; + $transitions = self::loadMultipleByProperties($entity_type, [$entity_id], $revision_ids, $field_name, $langcode, $limit, $sort, $transition_type); + if ($transitions) { + $transition = reset($transitions); + return $transition; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public static function loadMultipleByProperties($entity_type, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = 'workflow_transition') { + + /** @var $query \Drupal\Core\Entity\Query\QueryInterface */ + $query = \Drupal::entityQuery($transition_type) + ->condition('entity_type', $entity_type) + ->sort('timestamp', $sort) // 'DESC' || 'ASC' + ->addTag($transition_type); + if (!empty($entity_ids)) { + $query->condition('entity_id', $entity_ids, 'IN'); + } + if (!empty($revision_ids)) { + $query->condition('revision_id', $entity_ids, 'IN'); + } + if ($field_name != '') { + $query->condition('field_name', $field_name, '='); + } + if ($langcode != '') { + $query->condition('langcode', $langcode, '='); + } + if ($limit) { + $query->range(0, $limit); + } + if ($transition_type == 'workflow_transition') { + // The timestamp is only granular to the second; on a busy site, we need the id. + // $query->orderBy('h.timestamp', 'DESC'); + $query->sort('hid', 'DESC'); + } + $ids = $query->execute(); + $transitions = self::loadMultiple($ids); + return $transitions; + } + + /** + * Implementing interface WorkflowTransitionInterface - properties. + */ + + /** + * Determines if the Transition is valid and can be executed. + * @todo: add to isAllowed() ? + * @todo: add checks to WorkflowTransitionElement ? + * + * @return bool + */ + public function isValid() { + // Load the entity, if not already loaded. + // This also sets the (empty) $revision_id in Scheduled Transitions. + $entity = $this->getTargetEntity(); + + if (!$entity) { + // @todo: There is a watchdog error, but no UI-error. Is this OK? + $message = 'User tried to execute a Transition without an entity.'; + $this->logError($message); + return FALSE; // <-- exit !!! + } + if (!$this->getFromState()) { + // @todo: the page is not correctly refreshed after this error. + $message = t('You tried to set a Workflow State, but + the entity is not relevant. Please contact your system administrator.'); + drupal_set_message($message, 'error'); + $message = 'Setting a non-relevant Entity from state %sid1 to %sid2'; + $this->logError($message); + return FALSE; // <-- exit !!! + } + + // The transition is OK. + return TRUE; + } + + /** + * Check if anything has changed in this transition. + * @todo: check for attached fields. + * + * @return bool + */ + protected function isEmpty() { + if ($this->getToSid() != $this->getFromSid()) { + return FALSE; + } + if ($this->getComment()) { + return FALSE; + } + // @todo: check for attached fields. + return true; + } + + /** + * {@inheritdoc} + */ + public function isAllowed(UserInterface $user, $force = FALSE) { + /** + * Get early permissions of user, and bail out to avoid extra hook-calls. + */ + + if ($force) { + // $force allows Rules to cause transition. + return TRUE; + } + + // Determine if user is owner of the entity. + $is_owner = WorkflowManager::isOwner($user, $this->getTargetEntity()); + // Check allow-ability of state change if user is not superuser (might be cron). + $type_id = $this->getWorkflowId(); + if ($user->hasPermission("bypass $type_id workflow_transition access")) { + // Superuser is special. And $force allows Rules to cause transition. + return TRUE; + } + // Determine if user is owner of the entity. + if ($is_owner) { + $user->addRole(WORKFLOW_ROLE_AUTHOR_RID); + } + + // @todo: Keep below code aligned between WorkflowState, ~Transition, ~TransitionListController + /** + * Get the object and its permissions. + */ + $config_transitions = $this->getWorkflow()->getTransitionsByStateId($this->getFromSid(), $this->getToSid()); + + /** + * Determine if user has Access. + */ + $result = FALSE; + /** @var $config_transition WorkflowTransitionInterface */ + foreach ($config_transitions as $config_transition) { + $result = $result || $config_transition->isAllowed($user, $force); + } + + if ($result == FALSE) { + // @todo: There is a watchdog error, but no UI-error. Is this OK? + $message = t('Attempt to go to nonexistent transition (from %sid1 to %sid2)'); + $this->logError($message); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function execute($force = FALSE) { + // Load the entity, if not already loaded. + // This also sets the (empty) $revision_id in Scheduled Transitions. + /** @var $entity \Drupal\Core\Entity\EntityInterface */ + $entity = $this->getTargetEntity(); + $entity_id = $entity->id(); + // Load explicit User object (not via $transition) for adding Role later. + /** @var $user \Drupal\user\UserInterface */ + $user = $this->getOwner(); + $from_sid = $this->getFromSid(); + $to_sid = $this->getToSid(); + $field_name = $this->getFieldName(); + $comment = $this->getComment(); + // Create a label to identify this transition, + // even upon insert, when id() is not set, yet. + $label = $from_sid . '-' . $to_sid; + + static $static_info = NULL; + + if (isset($static_info[$entity_id][$field_name][$label])) { + // Error: this Transition is already executed. + // On the development machine, execute() is called twice, when + // on an Edit Page, the entity has a scheduled transition, and + // user changes it to 'immediately'. + // Why does this happen?? ( BTW. This happens with every submit.) + // Remedies: + // - search root cause of second call. + // - try adapting code of transition->save() to avoid second record. + // - avoid executing twice. + $message = 'Transition is executed twice in a call. The second call for + @entity_type %entity_id is not executed.'; + $this->logError($message); + + // Return the result of the last call. + return $static_info[$entity_id][$field_name][$label]; // <-- exit !!! + } + + // OK. Prepare for next round. Do not set last_sid!! + $static_info[$entity_id][$field_name][$label] = $from_sid; + + // Make sure $force is set in the transition, too. + if ($force) { + $this->force($force); + } + $force = $this->isForced(); + + // @todo D8-port: figure out usage of $entity->workflow_transitions[$field_name] + /* + // Store the transition, so it can be easily fetched later on. + // Store in an array, to prepare for multiple workflow_fields per entity. + // This is a.o. used in hook_entity_update to trigger 'transition post'. + $entity->workflow_transitions[$field_name] = $this; + */ + + if (!$this->isValid()) { + return $from_sid; // <-- exit !!! + } + + // @todo: move below code to $this->isAllowed(). + // If the state has changed, check the permissions. + // No need to check if Comments or attached fields are filled. + $state_changed = ($from_sid != $to_sid); + if ($state_changed) { + // Make sure this transition is allowed by workflow module Admin UI. + if (!$force) { + $user->addRole(WORKFLOW_ROLE_AUTHOR_RID); + } + if (!$this->isAllowed($user, $force)) { + $message = 'User %user not allowed to go from state %sid1 to %sid2'; + $this->logError($message); + return FALSE; // <-- exit !!! + } + + // Make sure this transition is valid and allowed for the current user. + // Invoke a callback indicating a transition is about to occur. + // Modules may veto the transition by returning FALSE. + // (Even if $force is TRUE, but they shouldn't do that.) + // P.S. The D7 hook_workflow 'transition permitted' is removed, in favour of below hook_workflow 'transition pre'. + $permitted = \Drupal::moduleHandler()->invokeAll('workflow', ['transition pre', $this, $user]); + // Stop if a module says so. + if (in_array(FALSE, $permitted, TRUE)) { + // @todo: There is a watchdog error, but no UI-error. Is this OK? + $message = 'Transition vetoed by module.'; + $this->logError($message, 'notice'); + return FALSE; // <-- exit !!! + } + } + + /** + * Output: process the transition. + */ + if ($this->isScheduled()) { + /* + * Log the transition in {workflow_transition_scheduled}. + */ + $this->save(); + } + else { + // The transition is allowed, but not scheduled. + // Let other modules modify the comment. The transition (in context) contains all relevant data. + $context = ['transition' => $this]; + \Drupal::moduleHandler()->alter('workflow_comment', $comment, $context); + $this->setComment($comment); + + $this->is_executed = TRUE; + + if (!$this->isEmpty()) { + /* + * Log the transition in {workflow_transition_history}. + */ + $this->save(); + + // Register state change with watchdog. + if ($state_changed && !empty($this->getWorkflow()->options['watchdog_log'])) { + if ($this->getEntityTypeId() == 'workflow_scheduled_transition') { + $message = 'Scheduled state change of @entity_type_label %entity_label to %sid2 executed'; + } + else { + $message = 'State of @entity_type_label %entity_label set to %sid2'; + } + $this->logError($message, 'notice'); + } + + // Notify modules that transition has occurred. + // Action triggers should take place in response to this callback, not the 'transaction pre'. + + //\Drupal::moduleHandler()->invokeAll('workflow', ['transition post', $this, $user]); + // We have a problem here with Rules, Trigger, etc. when invoking + // 'transition post': the entity has not been saved, yet. we are still + // IN the transition, not AFTER. Alternatives: + // 1. Save the field here explicitly, using field_attach_save; + // 2. Move the invoke to another place: hook_entity_insert(), hook_entity_update(); + // 3. Rely on the entity hooks. This works for Rules, not for Trigger. + // --> We choose option 2: + // @todo D8-port: figure out usage of $entity->workflow_transitions[$field_name] + // - First, $entity->workflow_transitions[] is set for easy re-fetching. + // - Then, post_execute() is invoked via workflow_entity_insert(), _update(). + } + } + + // Save value in static from top of this function. + $static_info[$entity_id][$field_name][$label] = $to_sid; + + return $to_sid; + } + + /** + * {@inheritdoc} + */ + public function executeAndUpdateEntity($force = FALSE) { + $to_sid = $this->getToSid(); + + + // Generate error and stop if transition has no new State. + if (!$to_sid) { + $t_args = [ + '%sid2' => $this->getToState()->label(), + '%entity_label' => $this->getTargetEntity()->label(), + ]; + $message = "Transition is not executed for %entity_label, since 'To' state %sid2 is invalid."; + $this->logError($message); + drupal_set_message(t($message, $t_args), 'error'); + + return $this->getFromSid(); + } + + // Save the (scheduled) transition. + $do_update_entity = (!$this->isScheduled() && !$this->isExecuted()); + if ($do_update_entity) { + $this->_updateEntity(); + } + else { + // We create a new transition, or update an existing one. + // Do not update the entity itself. + // Validate transition, save in history table and delete from schedule table. + $to_sid = $this->execute($force); + } + + return $to_sid; + } + + private function _updateEntity() { + // Update the workflow field of the entity. + $field_name = $this->getFieldName(); + $entity = $this->getTargetEntity(); + // N.B. Align the following functions: + // - WorkflowDefaultWidget::massageFormValues(); + // - WorkflowManager::executeTransition(). + $entity->$field_name->workflow_transition = $this; + $entity->$field_name->value = $this->getToSid(); + + $entity->save(); + } + + /** + * {@inheritdoc} + */ + public function post_execute($force = FALSE) { + // @todo D8-port: This function post_execute() is not yet used. + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: Test this snippet. + + if (!$this->isEmpty()) { + $user = $this->getOwner(); + \Drupal::moduleHandler()->invokeAll('workflow', ['transition post', $this, $user]); + } + } + + /** + * {@inheritdoc} + */ + public function getWorkflow() { + if (!$this->workflow && $wid = $this->getWorkflowId()) { + $this->workflow = Workflow::load($wid); + } + return $this->workflow; + } + + /** + * {@inheritdoc} + */ + public function getWorkflowId() { + + if (!$this->wid->target_id && $state = $this->getFromState()) { + // Fallback. + $state = ($state) ? $state : $this->getToState(); + $wid = ($state) ? $state->getWorkflowId() : ''; + + $this->set('wid', $wid); + } + return $this->wid->target_id; + } + + /** + * {@inheritdoc} + */ + public function setTargetEntity($entity) { + $this->entity_type = ''; + $this->entity_id = ''; + $this->revision_id = ''; + $this->delta = 0; // Only single value is supported. + $this->langcode = Language::LANGCODE_NOT_SPECIFIED; + + if (!$entity) { + return $this; + } + + // If Transition is added via CommentForm, use the Commented Entity. + if ($entity->getEntityTypeId() == 'comment') { + /** @var $entity \Drupal\comment\CommentInterface */ + $entity = $entity->getCommentedEntity(); + } + + $this->entity = $entity; + /** @var \Drupal\Core\Entity\RevisionableContentEntityBase $entity */ + $this->entity_type = $entity->getEntityTypeId(); + $this->entity_id = $entity->id(); + $this->revision_id = $entity->getRevisionId(); + $this->delta = 0; // Only single value is supported. + $this->langcode = $entity->language()->getId(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTargetEntity() { + // Use an explicit property, in case of adding new entities. + if (isset($this->entity)) { + return $this->entity; + } + // @todo D8: the following line only returns Node, not Term. + // return $this->entity = $this->get('entity_id')->entity; + + $entity_type = $this->getTargetEntityTypeId(); + if ($id = $this->getTargetEntityId()) { + $this->entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($id); + } + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function getTargetEntityId() { + return $this->get('entity_id')->target_id; + } + + /** + * {@inheritdoc} + */ + public function getTargetEntityTypeId() { + return $this->get('entity_type')->value; + } + + /** + * {@inheritdoc} + */ + public function getFieldName() { + return $this->get('field_name')->value; + } + + /** + * {@inheritdoc} + */ + public function getLangcode() { + return $this->getTargetEntity()->language()->getId(); + + } + + /** + * {@inheritdoc} + */ + public function getFromState() { + $sid = $this->getFromSid(); + return ($sid) ? WorkflowState::load($sid) : NULL; + } + + /** + * {@inheritdoc} + */ + public function getToState() { + $sid = $this->getToSid(); + return ($sid) ? WorkflowState::load($sid) : NULL; + } + + /** + * {@inheritdoc} + */ + public function getFromSid() { + $sid = $this->{'from_sid'}->target_id; + return $sid; + } + + /** + * {@inheritdoc} + */ + public function getToSid() { + $sid = $this->{'to_sid'}->target_id; + return $sid; + } + + /** + * {@inheritdoc} + */ + public function getComment() { + return $this->get('comment')->value; + } + + /** + * {@inheritdoc} + */ + public function setComment($value) { + $this->set('comment', $value); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTimestamp() { + return $this->get('timestamp')->value; + } + + /** + * {@inheritdoc} + */ + public function getTimestampFormatted() { + $timestamp = $this->getTimestamp(); + return \Drupal::service('date.formatter')->format($timestamp); + } + + /** + * {@inheritdoc} + */ + public function setTimestamp($value) { + $this->set('timestamp', $value); + return $this; + } + + /** + * {@inheritdoc} + */ + public function isScheduled() { + return $this->is_scheduled; + } + + /** + * {@inheritdoc} + */ + public function schedule($schedule = TRUE) { +// // We do a tricky thing here. The id of the entity is altered, so +// // all functions of another subclass are called. +// $this->entityTypeId = ($schedule) ? 'workflow_scheduled_transition' : 'workflow_transition'; + + $this->is_scheduled = $schedule; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setExecuted($is_executed = TRUE) { + $this->is_executed = $is_executed; + return $this; + } + + /** + * {@inheritdoc} + */ + public function isExecuted() { + return (bool) $this->is_executed; + } + + /** + * {@inheritdoc} + */ + public function isForced() { + return (bool) $this->is_forced; + } + + /** + * {@inheritdoc} + */ + public function force($force = TRUE) { + $this->is_forced = $force; + return $this; + } + + /** + * Implementing interface EntityOwnerInterface. Copied from Comment.php. + */ + + /** + * {@inheritdoc} + */ + public function getOwner() { + /** @var $user UserInterface */ + $user = $this->get('uid')->entity; + if (!$user || $user->isAnonymous()) { + $user = User::getAnonymousUser(); + $user->setUsername(\Drupal::config('user.settings')->get('anonymous')); + } + return $user; + } + + /** + * {@inheritdoc} + */ + public function getOwnerId() { + return $this->get('uid')->target_id; + } + + /** + * {@inheritdoc} + */ + public function setOwnerId($uid) { + $this->set('uid', $uid); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOwner(UserInterface $account) { + $this->set('uid', $account->id()); + return $this; + } + + /** + * Implementing interface FieldableEntityInterface extends EntityInterface. + */ + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = []; + + $fields['hid'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Transition ID')) + ->setDescription(t('The transition ID.')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + +// $fields['wid'] = BaseFieldDefinition::create('string') + $fields['wid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Workflow Type')) + ->setDescription(t('The workflow type the transition relates to.')) + ->setSetting('target_type', 'workflow_type') + ->setRequired(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(FALSE) +// ->setSetting('max_length', 32) +// ->setDisplayOptions('view', [ +// 'label' => 'hidden', +// 'type' => 'string', +// 'weight' => -5, +// ]) +// ->setDisplayOptions('form', [ +// 'type' => 'string_textfield', +// 'weight' => -5, +// ]) +// ->setDisplayConfigurable('form', TRUE) + ; + + $fields['entity_type'] = BaseFieldDefinition::create('string') + ->setLabel(t('Entity type')) + ->setDescription(t('The Entity type this transition belongs to.')) + ->setSetting('is_ascii', TRUE) + ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH) + ->setReadOnly(TRUE); + + $fields['entity_id'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Entity ID')) + ->setDescription(t('The Entity ID this record is for.')) + ->setRequired(TRUE) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + + $fields['revision_id'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Revision ID')) + ->setDescription(t('The current version identifier.')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + + $fields['field_name'] = BaseFieldDefinition::create('string') + ->setLabel(t('Field name')) + ->setDescription(t('The name of the field the transition relates to.')) + ->setRequired(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(FALSE) + ->setSetting('max_length', 32) +// ->setDisplayConfigurable('form', FALSE) +// ->setDisplayOptions('form', [ +// 'type' => 'string_textfield', +// 'weight' => -5, +// ]) +// ->setDisplayConfigurable('view', FALSE) +// ->setDisplayOptions('view', [ +// 'label' => 'hidden', +// 'type' => 'string', +// 'weight' => -5, +// ]) + ; + + $fields['langcode'] = BaseFieldDefinition::create('language') + ->setLabel(t('Language')) + ->setDescription(t('The entity language code.')) + ->setTranslatable(TRUE) + ->setDisplayOptions('view', [ + 'type' => 'hidden', + ]) + ->setDisplayOptions('form', [ + 'type' => 'language_select', + 'weight' => 2, + ]); + + $fields['delta'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Delta')) + ->setDescription(t('The sequence number for this data item, used for multi-value fields.')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + + $fields['from_sid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('From state')) + ->setDescription(t('The {workflow_states}.sid the entity started as.')) + ->setSetting('target_type', 'workflow_state') + ->setReadOnly(TRUE); + + $fields['to_sid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('To state')) + ->setDescription(t('The {workflow_states}.sid the entity transitioned to.')) + ->setSetting('target_type', 'workflow_state') +// @todo D8: activate this. Test with both Form and Widget. +// ->setDisplayOptions('form', [ +// 'type' => 'select', +// 'weight' => -5, +// ]) +// ->setDisplayConfigurable('form', TRUE) + ->setReadOnly(TRUE); + + $fields['uid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('User ID')) + ->setDescription(t('The user ID of the transition author.')) + ->setTranslatable(TRUE) + ->setSetting('target_type', 'user') + ->setDefaultValue(0) +// ->setQueryable(FALSE) +// ->setSetting('handler', 'default') +// ->setDefaultValueCallback('Drupal\node\Entity\Node::getCurrentUserId') +// ->setTranslatable(TRUE) +// ->setDisplayOptions('view', [ +// 'label' => 'hidden', +// 'type' => 'author', +// 'weight' => 0, +// ]) +// ->setDisplayOptions('form', [ +// 'type' => 'entity_reference_autocomplete', +// 'weight' => 5, +// 'settings' => [ +// 'match_operator' => 'CONTAINS', +// 'size' => '60', +// 'placeholder' => '', +// ], +// ]) +// ->setDisplayConfigurable('form', TRUE), + ->setRevisionable(TRUE); + + $fields['timestamp'] = BaseFieldDefinition::create('created') + ->setLabel(t('Timestamp')) + ->setDescription(t('The time that the current transition was executed.')) +// ->setQueryable(FALSE) +// ->setTranslatable(TRUE) +// ->setDisplayOptions('view', [ +// 'label' => 'hidden', +// 'type' => 'timestamp', +// 'weight' => 0, +// ]) +// @todo D8: activate this. Test with both Form and Widget. +// ->setDisplayOptions('form', [ +// 'type' => 'datetime_timestamp', +// 'weight' => 10, +// ]) +// ->setDisplayConfigurable('form', TRUE); + ->setRevisionable(TRUE); + + $fields['comment'] = BaseFieldDefinition::create('string_long') + ->setLabel(t('Log message')) + ->setDescription(t('The comment explaining this transition.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE) +// @todo D8: activate this. Test with both Form and Widget. +// ->setDisplayOptions('form', [ +// 'type' => 'string_textarea', +// 'weight' => 25, +// 'settings' => [ +// 'rows' => 4, +// ], +// ]) +// ->setDisplayConfigurable('form', FALSE) + ; + + return $fields; + } + + /** + * Generate a Watchdog error + * + * @param $message + * @param $type {'error' | 'notice'} + * @param $from_sid + * @param $to_sid + */ + public function logError($message, $type = 'error', $from_sid = '', $to_sid = '') { + + // Prepare an array of arguments for error messages. + $entity = $this->getTargetEntity(); + $t_args = [ + /** @var $user \Drupal\user\UserInterface */ + '%user' => ($user = $this->getOwner()) ? $user->getDisplayName() : '', + '%sid1' => ($from_sid) ? $from_sid : $this->getFromState()->label(), + '%sid2' => ($to_sid) ? $to_sid : $this->getToState()->label(), + '%entity_id' => $this->getTargetEntityId(), + '%entity_label' => $entity ? $entity->label() : '', + '@entity_type' => ($entity) ? $entity->getEntityTypeId() : '', + '@entity_type_label' => ($entity) ? $entity->getEntityType()->getLabel() : '', + 'link' => ($this->getTargetEntityId() && $this->getTargetEntity()->hasLinkTemplate('canonical')) ? $this->getTargetEntity()->toLink(t('View'))->toString() : '', + ]; + ($type == 'error') ? \Drupal::logger('workflow')->error($message, $t_args) + : \Drupal::logger('workflow')->notice($message, $t_args); + } + + /** + * {@inheritdoc} + */ + public function dpm($function = '') { + $transition = $this; + $entity = $transition->getTargetEntity(); + $time = \Drupal::service('date.formatter')->format($transition->getTimestamp()); + // Do this extensive $user_name lines, for some troubles with Action. + $user = $transition->getOwner(); + $user_name = ($user) ? $user->getAccountName() : 'unknown username'; + $t_string = $this->getEntityTypeId() . ' ' . $this->id() . ' for workflow_type ' . $this->getWorkflowId() . ' ' . ($function ? ("in function '$function'") : ''); + $output[] = 'Entity = ' . $this->getTargetEntityTypeId() . '/' . (($entity) ? ($entity->bundle() . '/' . $entity->id()) : '___/0') ; + $output[] = 'Field = ' . $transition->getFieldName(); + $output[] = 'From/To = ' . $transition->getFromSid() . ' > ' . $transition->getToSid() . ' @ ' . $time; + $output[] = 'Comment = ' . $user_name . ' says: ' . $transition->getComment(); + $output[] = 'Forced = ' . ($transition->isForced() ? 'yes' : 'no') .'; ' . 'Scheduled = ' . ($transition->isScheduled() ? 'yes' : 'no'); + if (function_exists('dpm')) { dpm($output, $t_string); } + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransitionInterface.php b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransitionInterface.php new file mode 100644 index 000000000..ad55871c8 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Entity/WorkflowTransitionInterface.php @@ -0,0 +1,277 @@ + $field_name]); + * $transition->setTargetEntity($entity); + * $transition->setValues($new_sid, $user->id(), REQUEST_TIME, $comment); + * + * @param string $to_sid + * @param int $uid + * @param int $timestamp + * @param string $comment + * @param bool $force_create + */ + public function setValues($to_sid, $uid = NULL, $timestamp = NULL, $comment = '', $force_create = FALSE); + + /** + * Load (Scheduled) WorkflowTransitions, most recent first. + * + * @param string $entity_type + * @param int $entity_id + * @param array $revision_ids + * @param string $field_name + * @param string $langcode + * @param string $sort + * @param string $transition_type + * + * @return \Drupal\workflow\Entity\WorkflowTransitionInterface + * Object representing one row from the {workflow_transition_history} table. + */ + public static function loadByProperties($entity_type, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = ''); + + /** + * Given an entity, get all transitions for it. + * + * Since this may return a lot of data, a limit is included to allow for only one result. + * + * @param string $entity_type + * @param int[] $entity_ids + * @param int[] $revision_ids + * @param string $field_name + * Optional. Can be NULL, if you want to load any field. + * @param string $langcode + * Optional. Can be empty, if you want to load any language. + * @param int $limit + * Optional. Can be NULL, if you want to load all transitions. + * @param string $sort + * Optional sort order. {'ASC'|'DESC'} + * @param string $transition_type + * The type of the transition to be fetched. + * + * @return WorkflowTransitionInterface[] + * An array of transitions. + */ + public static function loadMultipleByProperties($entity_type, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = ''); + + /** + * Execute a transition (change state of an entity). + * + * A Scheduled Transition shall only be saved, unless the + * 'schedule' property is set. + * @usage + * $transition->schedule(FALSE); + * $to_sid = $transition->execute(TRUE); + * + * @param bool $force + * If set to TRUE, workflow permissions will be ignored. + * + * @return + * New state ID. If execution failed, old state ID is returned, + */ + public function execute($force = FALSE); + + /** + * Executes a transition (change state of an entity), from OUTSIDE the entity. + * + * Use $transition->executeAndUpdateEntity() to start a State Change from + * outside an entity, e.g., workflow_cron(). + * Use $transition->execute() to start a State Change from within an entity. + * + * A Scheduled Transition ($transition->isScheduled() == TRUE) will be + * un-scheduled and saved in the history table. + * The entity will not be updated. + * If $transition->isScheduled() == FALSE, the Transition will be + * removed from the {workflow_transition_scheduled} table (if necessary), + * and added to {workflow_transition_history} table. + * Then the entity wil be updated to reflect the new status. + * + * @usage + * $to_sid = $transition->->executeAndUpdateEntity($force); + * + * @see workflow_execute_transition() + * + * @param bool $force + * If set to TRUE, workflow permissions will be ignored. + * + * @return string + * The resulting WorkflowState id. + */ + public function executeAndUpdateEntity($force = FALSE); + + /** + * Invokes 'transition post'. + * Adds the possibility to invoke the hook from elsewhere. + * + * @param bool $force + */ + public function post_execute($force = FALSE); + + /** + * Set the Entity, that is added to the Transition. + * Also set all dependent fields, that will be saved in tables {workflow_transition_*} + * + * @param EntityInterface $entity + * The Entity ID or the Entity object, to add to the Transition. + * + * @return object + * The Entity, that is added to the Transition. + */ + public function setTargetEntity($entity); + + /** + * Returns the entity to which the workflow is attached. + * + * @return EntityInterface + * The entity to which the workflow is attached. + */ + public function getTargetEntity(); + + /** + * Returns the ID of the entity to which the workflow is attached. + * + * @return int + * The ID of the entity to which the workflow is attached. + */ + public function getTargetEntityId(); + + /** + * Returns the type of the entity to which the workflow is attached. + * + * @return string + * An entity type. + */ + public function getTargetEntityTypeId(); + + /** + * Get the field_name for which the Transition is valid. + * + * @return string + * The field_name, that is added to the Transition. + */ + public function getFieldName(); + + /** + * Get the language code for which the Transition is valid. + * + * @todo: OK?? Shouldn't we use entity's language() method for langcode? + * + * @return string $langcode + */ + public function getLangcode(); + + /** + * Get the comment of the Transition. + * + * @return + * The comment + */ + public function getComment(); + + /** + * Get the comment of the Transition. + * + * @param $value + * The new comment. + * + * @return WorkflowTransitionInterface + */ + public function setComment($value); + + /** + * Returns the time on which the transitions was or will be executed. + * + * @return + */ + public function getTimestamp(); + + /** + * Returns the human-readable time. + * + * @return string + */ + public function getTimestampFormatted(); + + /** + * Returns the time on which the transitions was or will be executed. + * + * @param $value + * The new timestamp. + * @return WorkflowTransitionInterface + */ + public function setTimestamp($value); + + /** + * Returns if this is a Scheduled Transition. + * + * @return bool + */ + public function isScheduled(); + + /** + * Sets the Transition to be scheduled or not. + * + * @param bool $schedule + * @return WorkflowTransitionInterface + */ + public function schedule($schedule = TRUE); + + /** + * Set the 'is_executed' property. + * + * @param bool $is_executed + * + * @return WorkflowTransitionInterface + */ + public function setExecuted($is_executed = TRUE); + + /** + * Returns if this is an Executed Transition. + * + * @return bool + */ + public function isExecuted(); + + /** + * A transition may be forced skipping checks. + * + * @return bool + * If the transition is forced. (Allow not-configured transitions). + */ + public function isForced(); + + /** + * Set if a transition must be executed, even if transition is invalid + * or user not authorized. + * + * @param bool $force + * + * @return object + * The transition itself + */ + public function force($force = TRUE); + + /** + * Helper/debugging function. Shows simple contents of Transition. + * + * @param string $function + */ + public function dpm($function = ''); + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTransitionForm.php b/sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTransitionForm.php new file mode 100644 index 000000000..680d1b54a --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTransitionForm.php @@ -0,0 +1,211 @@ +entity; + $field_name = $transition->getFieldName(); + + /** @var $entity \Drupal\Core\Entity\EntityInterface */ + // Entity may be empty on VBO bulk form. + // $entity = $transition->getTargetEntity(); + // Compose Form Id from string + Entity Id + Field name. + // Field ID contains entity_type, bundle, field_name. + // The Form Id is unique, to allow for multiple forms per page. + // $workflow_type_id = $transition->getWorkflowId(); + // Field name contains implicit entity_type & bundle (since 1 field per entity) + // $entity_type = $transition->getTargetEntityTypeId(); + // $entity_id = $transition->getTargetEntityId();; + + // Emulate nodeForm convention. + if ($transition->id()) { + $suffix = 'edit_form'; + } + else { + $suffix = 'form'; + } + $form_id = implode('_', ['workflow_transition', $field_name, $suffix]); + $form_id = Html::getUniqueId($form_id); + + return $form_id; + } + + /** + * {@inheritdoc} + * + * N.B. The D8-version of this form is stripped. If any use case is missing: + * - compare with the D7-version of WorkflowTransitionForm::submitForm() + * - compare with the D8-version of WorkflowTransitionElement::copyFormValuesToEntity() + */ + /* + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + } + */ + + /* ************************************************************************* + * + * Implementation of interface EntityFormInterface (extends FormInterface). + * + */ + + /** + * This function is called by buildForm(). + * Caveat: !! It is not declared in the EntityFormInterface !! + * + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + // Call parent to get (extra) fields. + // This might cause baseFieldDefinitions to appear twice. + $form = parent::form($form, $form_state); + + /** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */ + $transition = $this->entity; + + // Do not pass the element, but the form. + // $element['#default_value'] = $transition; + // $form += WorkflowTransitionElement::transitionElement($element, $form_state, $form); + // + // Pass the form via parameter + $form['#default_value'] = $transition; + $form = WorkflowTransitionElement::transitionElement($form, $form_state, $form); + return $form; + } + + /* + * Returns the action form element for the current entity form. + * Caveat: !! It is not declared in the EntityFormInterface !! + * + * {@inheritdoc} + protected function actionsElement(array $form, FormStateInterface $form_state) { + $element = parent::actionsElement($form, $form_state); + return $element; + } + */ + + /** + * Returns an array of supported actions for the current entity form. + * Caveat: !! It is not declared in the EntityFormInterface !! + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @return array + */ + protected function actions(array $form, FormStateInterface $form_state) { + // N.B. Keep code aligned: workflow_form_alter(), WorkflowTransitionForm::actions(). + $actions = parent::actions($form, $form_state); + + // A default button is provided by core. Override it. + $actions['submit']['#value'] = t('Update workflow'); + $actions['submit']['#attributes'] = ['class' => ['form-save-default-button']]; + + if (!_workflow_use_action_buttons()) { + // Change the default submit button on the Workflow History tab. + return $actions; + } + + // Find the first workflow. + // (So this won't work with multiple workflows per entity.) + // Quit if there is no Workflow on this page. + if (!$workflow_form = &$form) { + return $actions; + } + + // Quit if there are no Workflow Action buttons. + // (If user has only 1 workflow option, there are no Action buttons.) + if (count($workflow_form['to_sid']['#options']) <= 1) { + return $actions; + } + + // Place the buttons. Remove the default 'Save' button. + // $actions += _workflow_transition_form_get_action_buttons($form, $workflow_form); + // Remove the default submit button from the form. + // unset($actions['submit']); + $default_submit_action = $actions['submit']; + $actions = _workflow_transition_form_get_action_buttons($form, $workflow_form, $default_submit_action); + foreach ($actions as $key => &$action) { + $action['#submit'] = $default_submit_action['#submit']; + } + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function buildEntity(array $form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */ + $entity = clone $this->entity; + // Update the entity. + $entity = $this->copyFormValuesToEntity($entity, $form, $form_state); + // Mark the entity as NOT requiring validation. (Used in validateForm().) + $entity->setValidationRequired(FALSE); + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + parent::copyFormValuesToEntity($entity, $form, $form_state); + // Use a proprietary version of copyFormValuesToEntity(), passing $entity by reference... + $values = $form_state->getValues(); + // ... but only the returning object is OK (!). + return WorkflowTransitionElement::copyFormValuesToTransition($entity, $form, $form_state, $values); + } + + /** + * {@inheritdoc} + * + * This is called from submitForm(). + */ + public function save(array $form, FormStateInterface $form_state) { + // Execute transition and update the attached entity. + /** @var WorkflowTransitionInterface $transition */ + $transition = $this->getEntity(); + return $transition->executeAndUpdateEntity(); + } + + /************************************************************************* + * + * Implementation of interface ContentEntityFormInterface (extends EntityFormInterface). + * + */ + + /** + * {@inheritdoc} + */ + //public function validateForm(array &$form, FormStateInterface $form_state) { + // return parent::validateForm($form, $form_state); + //} + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTypeForm.php b/sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTypeForm.php new file mode 100644 index 000000000..db93357fa --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Form/WorkflowTypeForm.php @@ -0,0 +1,247 @@ + t('No'), 1 => t('Yes')]; + $fieldset_options = [0 => t('No fieldset'), 1 => t('Collapsible fieldset'), 2 => t('Collapsed fieldset')]; + /* @var $workflow Workflow */ + $workflow = $this->entity; + + $form['label'] = [ + '#type' => 'textfield', + '#description' => t('The human-readable label of the workflow. This will be used as a label when + the workflow status is shown during editing of content.'), + '#title' => $this->t('Label'), + '#default_value' => $this->entity->label(), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'), + '#disabled' => !$this->entity->isNew(), + '#default_value' => $this->entity->id(), + '#machine_name' => [ + 'exists' => [$this, 'exists'], + 'replace_pattern' => '([^a-z0-9_]+)|(^custom$)', + 'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".'), + ], + ]; + + $form['permissions'] = [ + '#type' => 'details', + '#title' => t('Workflow permissions'), + '#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE. + '#description' => t("To enable further Workflow functionality, go to the + /admin/people/permissions page and select any roles that should have + access to below and other functionalities."), + ]; + $form['permissions']['transition_execute'] = [ + '#type' => 'item', + '#title' => t('Participate in workflow (create, execute transitions)'), + '#markup' => t("You can determine which roles are enabled on the + 'Workflow Transitions & roles' configuration page. Use this to enable + only the relevant roles. Some sites have too many roles to show on + the configuration page."), + ]; + $form['permissions']['transition_schedule'] = [ + '#type' => 'item', + '#title' => t('Schedule state transition'), + '#markup' => t("Workflow transitions may be scheduled to a moment in the + future. Soon after the desired moment, the transition is executed by + Cron. This may be hidden by settings in widgets, formatters or permissions." + ), + ]; + $form['permissions']['history_tab'] = [ + '#type' => 'item', + '#title' => t('Access Workflow history tab'), + '#markup' => t("You can determine if a tab 'Workflow history' is + shown on the entity view page, which gives access to the History of + the workflow. + If you have multiple workflows per bundle, better disable this feature, + and use, clone & adapt the Views display 'Workflow history per Entity'."), + ]; + + $form['basic'] = [ + '#type' => 'details', + '#title' => t('Workflow form settings'), + // '#description' => t('Lorem ipsum.'), + '#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE. + ]; + + $form['basic']['fieldset'] = [ + '#type' => 'select', + '#options' => $fieldset_options, + '#title' => t('Show the form in a fieldset?'), + '#default_value' => isset($workflow->options['fieldset']) ? $workflow->options['fieldset'] : 0, + ]; + $form['basic']['options'] = [ + '#type' => 'select', + '#title' => t('How to show the available states'), + '#required' => FALSE, + '#default_value' => isset($workflow->options['options']) ? $workflow->options['options'] : 'radios', + // '#multiple' => TRUE / FALSE, + '#options' => [ + // These options are taken from options.module + 'select' => 'Select list', + 'radios' => 'Radio buttons', + 'buttons' => 'Action buttons', + 'dropbutton' => 'Drop button', + ], + '#description' => t("The Widget shows all available states. Decide which + is the best way to show them." + ), + ]; + + $form['basic']['name_as_title'] = [ + '#type' => 'checkbox', + '#attributes' => ['class' => ['container-inline']], + '#title' => t('Use the workflow name as the title of the workflow form'), + '#default_value' => isset($workflow->options['name_as_title']) ? $workflow->options['name_as_title'] : 0, + '#description' => t( + 'The workflow section of the editing form is in its own fieldset. + Checking the box will add the workflow name as the title of workflow + section of the editing form.' + ), + ]; + + $form['basic']['schedule_timezone'] = [ + '#type' => 'checkbox', + '#title' => t('Show a timezone when scheduling a transition.'), + '#required' => FALSE, + '#default_value' => isset($workflow->options['schedule_timezone']) ? $workflow->options['schedule_timezone'] : 1, + ]; + + $form['basic']['comment_log_node'] = [ + '#type' => 'select', + '#required' => FALSE, + '#options' => [ + // Use 0/1/2 to stay compatible with previous checkbox. + 0 => t('hidden'), + 1 => t('optional'), + 2 => t('required'), + ], + '#attributes' => ['class' => ['container-inline']], + '#title' => t('How to show the Comment subfield'), + '#default_value' => isset($workflow->options['comment_log_node']) ? $workflow->options['comment_log_node'] : 1, + '#description' => t( + 'A Comment area can be shown on the Workflow Transition form so that the person + making a state change can record reasons for doing so. The comment is + then included in the content\'s workflow history.' + ), + ]; + + $form['watchdog'] = [ + '#type' => 'details', + '#title' => t('Watchdog'), + '#description' => t('Informational watchdog messages can be logged when a transition is executed (state of a node is changed).'), + '#open' => TRUE, // Controls the HTML5 'open' attribute. Defaults to FALSE. + ]; + + $form['watchdog']['watchdog_log'] = [ + '#type' => 'checkbox', + '#title' => t('Log watchdog messages upon state change'), + '#default_value' => isset($workflow->options['watchdog_log']) ? $workflow->options['watchdog_log'] : 0, + '#description' => '', + ]; + + return parent::form($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + // $actions['submit']['#value'] = $this->t('Save'); + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /* @var $entity \Drupal\workflow\Entity\Workflow */ + $entity = $this->entity; + + // Prevent leading and trailing spaces. + $entity->set('label', trim($entity->label())); + + $entity->set('options', [ + 'name_as_title' => $form_state->getValue('name_as_title'), + 'fieldset' => $form_state->getValue('fieldset'), + 'options' => $form_state->getValue('options'), + 'schedule_timezone' => $form_state->getValue('schedule_timezone'), + 'comment_log_node' => $form_state->getValue('comment_log_node'), + 'watchdog_log' => $form_state->getValue('watchdog_log'), + ] + ); + + $status = parent::save($form, $form_state); + $action = $status == SAVED_UPDATED ? 'updated' : 'added'; + + // Tell the user we've updated the data. + $args = [ + '%label' => $entity->label(), + '%action' => $action, + 'link' => $entity->toLink(t('Edit'))->toString(), + ]; + drupal_set_message($this->t('Workflow %label has been %action. Please maintain the permissions, states and transitions.', $args)); + $this->logger('workflow')->notice('Workflow %label has been %action.', $args); + + if ($status == SAVED_NEW) { + $form_state->setRedirect('entity.workflow_type.edit_form', ['workflow_type' => $entity->id()]); + } + + } + + /** + * Form validation handler. + * + * @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 validateForm(array &$form, FormStateInterface $form_state) { + $workflow = $this->entity; + $name = $workflow->id(); + + // Make sure workflow name is not numeric. + // TODO: this was a prerequisite in D7. Remove in D8? + if (ctype_digit($name)) { + $form_state->setErrorByName('id', t('Please choose a non-numeric name for your workflow.')); + } + + parent::validateForm($form, $form_state); + } + + /** + * Machine name exists callback. + * + * @param string $id + * The machine name ID. + * + * @return bool + * TRUE if an entity with the same name already exists, FALSE otherwise. + */ + public function exists($id) { + $type = $this->entity->getEntityTypeId(); + + return (bool) $this->entityTypeManager->getStorage($type)->load($id); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeGivenStateAction.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeGivenStateAction.php new file mode 100644 index 000000000..b12bbcfa7 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeGivenStateAction.php @@ -0,0 +1,79 @@ + ['workflow', 'node'], + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + /* + * D7: As advanced action with Trigger 'node': + * - $entity is empty; + * - $context['group'] = 'node' + * - $context['hook'] = 'node_insert / _update / _delete' + * - $context['node'] = (Object) stdClass + * - $context['entity_type'] = NULL + * + * D7: As advanced action with Trigger 'taxonomy': + * - $entity is (Object) stdClass; + * - $context['type'] = 'entity' + * - $context['group'] = 'taxonomy' + * - $context['hook'] = 'taxonomy_term_insert / _update / _delete' + * - $context['node'] = (Object) stdClass + * - $context['entity_type'] = NULL + * + * D7: As advanced action with Trigger 'workflow API': + * ... + * + * D7: As VBO action: + * - $entity is (Object) stdClass; + * - $context['type'] = NULL + * - $context['group'] = NULL + * - $context['hook'] = NULL + * - $context['node'] = (Object) stdClass + * - $context['entity_type'] = 'node' + */ + + if (!$transition = $this->getTransitionForExecution($object)) { + drupal_set_message('The entity is not valid for this action.'); + return; + } + + // Fire the transition. + workflow_execute_transition($transition, $force); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeNextStateAction.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeNextStateAction.php new file mode 100644 index 000000000..87caa85f3 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowNodeNextStateAction.php @@ -0,0 +1,71 @@ + ['workflow', 'node'], + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + + // Remove to_sid. User can't set it, since we want a dynamic 'next' state. + unset($form['to_sid']); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + + if (!$transition = $this->getTransitionForExecution($object)) { + drupal_set_message('The object is not valid for this action.', 'warning'); + return; + } + + /* + * Set the new next state. + */ + $entity = $transition->getTargetEntity(); + $field_name = $transition->getFieldName(); + $user = $transition->getOwner(); + $force = $this->configuration['force']; + // $comment = $transition->getComment(); + + // Get the node's new State Id (which is the next available state). + $to_sid = $transition->getWorkflow()->getNextSid($entity, $field_name, $user, $force); + + // Add actual data. + $transition->to_sid = $to_sid; + + // Fire the transition. + workflow_execute_transition($transition, $force); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowStateActionBase.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowStateActionBase.php new file mode 100644 index 000000000..83c0b27e2 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Action/WorkflowStateActionBase.php @@ -0,0 +1,232 @@ + ['workflow',], + ]; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $configuration = parent::defaultConfiguration(); + $configuration += $this->configuration; + $configuration += [ + 'field_name' => '', + 'to_sid' => '', + 'comment' => "New state is set by a triggered Action.", + 'force' => 0, + ]; + return $configuration; + } + + /** + * @param EntityInterface $entity + * @return \Drupal\workflow\Entity\WorkflowTransitionInterface + */ + protected function getTransitionForExecution(EntityInterface $entity) { + $user = workflow_current_user(); + + if (!$entity) { + \Drupal::logger('workflow_action')->notice('Unable to get current entity - entity is not defined.', []); + return NULL; + } + + // Get the entity type and numeric ID. + $entity_id = $entity->id(); + if (!$entity_id) { + \Drupal::logger('workflow_action')->notice('Unable to get current entity ID - entity is not yet saved.', []); + return NULL; + } + + // In 'after saving new content', the node is already saved. Avoid second insert. + // @todo: clone? + $entity->enforceIsNew(FALSE); + + $config = $this->configuration; + $field_name = workflow_get_field_name($entity, $config['field_name']); + $current_sid = workflow_node_current_state($entity, $field_name); + if (!$current_sid) { + \Drupal::logger('workflow_action')->notice('Unable to get current workflow state of entity %id.', ['%id' => $entity_id]); + return NULL; + } + + $to_sid = isset($config['to_sid']) ? $config['to_sid'] : ''; + // Get the Comment. Parse the $comment variables. + $comment_string = $this->configuration['comment']; + $comment = t($comment_string, [ + '%title' => $entity->label(), + // "@" and "%" will automatically run check_plain(). + '%state' => workflow_get_sid_name($to_sid), + '%user' => $user->getDisplayName(), + ]); + $force = $this->configuration['force']; + + $transition = WorkflowTransition::create([$current_sid, 'field_name' => $field_name]); + $transition->setTargetEntity($entity); + $transition->setValues($to_sid, $user->id(), \Drupal::time()->getRequestTime(), $comment); + $transition->force($force); + + return $transition; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = []; + + // If we are on admin/config/system/actions and use CREATE AN ADVANCED ACTION + // Then $context only contains: + // - $context['actions_label'] = "Change workflow state of post to new state" + // - $context['actions_type'] = "entity" + // + // If we are on a VBO action form, then $context only contains: + // - $context['entity_type'] = "node" + // - $context['view'] = "(Object) view" + // - $context['settings'] = "[]" + + $config = $this->configuration; + $field_name = $config['field_name']; + $wids = workflow_get_workflow_names(); + + if (empty($field_name) && count($wids) > 1) { + drupal_set_message('You have more then one workflow in the system. Please first select the field name + and save the form. Then, revisit the form to set the correct state value.', 'warning'); + } + if (empty($field_name)) { + $wid = count($wids) ? array_keys($wids)[0] : ''; + } + else { + $fields = _workflow_info_fields($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name); + $wid = count($fields) ? reset($fields)->getSetting('workflow_type') : ''; + } + + // Get the common Workflow, or create a dummy Workflow. + $workflow = $wid ? Workflow::load($wid) : Workflow::create(['id' => 'dummy_action', 'label' => 'dummy_action']); + $current_state = $workflow->getCreationState(); + + /* + // @todo D8-port for VBO + // Show the current state and the Workflow form to allow state changing. + // N.B. This part is replicated in hook_node_view, workflow_tab_page, workflow_vbo. + if ($workflow) { + $field = _workflow_info_field($field_name, $workflow); + $field_name = $field['field_name']; + $field_id = $field['id']; + $instance = field_info_instance($entity_type, $field_name, $entity_bundle); + + // Hide the submit button. VBO has its own 'next' button. + $instance['widget']['settings']['submit_function'] = ''; + if (!$field_id) { + // This is a Workflow Node workflow. Set widget options as in v7.x-1.2 + $field['settings']['widget']['comment'] = $workflow->options['comment_log_node']; // 'comment_log_tab' is removed; + $field['settings']['widget']['current_status'] = TRUE; + // As stated above, the options list is probably very long, so let's use select list. + $field['settings']['widget']['options'] = 'select'; + // Do not show the default [Update workflow] button on the form. + $instance['widget']['settings']['submit_function'] = ''; + } + } + + // Add the form/widget to the formatter, and include the nid and field_id in the form id, + // to allow multiple forms per page (in listings, with hook_forms() ). + // Ultimately, this is a wrapper for WorkflowDefaultWidget. + // $form['workflow_current_state'] = workflow_state_formatter($entity_type, $entity, $field, $instance); + $form_id = implode('_', [ + 'workflow_transition_form', + $entity_type, + $entity_id, + $field_id + ]); + */ + $user = workflow_current_user(); + $transition = WorkflowTransition::create([$current_state, 'field_name' => $field_name]); + $transition->setValues( + $to_sid = $config['to_sid'], + $user->id(), + \Drupal::time()->getRequestTime(), + $comment = $config['comment'], + $force = $config['force'] + ); + + // Add the WorkflowTransitionForm to the page. @todo + + $element = []; // Just to be explicit. + $element['#default_value'] = $transition; + + // Avoid Action Buttons. That removes the options box&more. No Buttons in config screens! + $original_options = $transition->getWorkflow()->options['options']; + $transition->getWorkflow()->options['options'] = 'select'; + // Generate and add the Workflow form element. + $element = WorkflowTransitionElement::transitionElement($element, $form_state, $form); + // Just to be sure, reset the options box setting. + $transition->getWorkflow()->options['options'] = $original_options; + + // Make adaptations for VBO-form: + $element['field_name']['#access'] = TRUE; + $element['force']['#access'] = TRUE; + $element['to_sid']['#description'] = t('Please select the state that should be assigned when this action runs.'); + $element['comment']['#title'] = $this->t('Message'); + $element['comment']['#description'] = $this->t('This message will be written into the workflow history log when the action + runs. You may include the following variables: %state, %title, %user.'); + + $form['workflow_transition_action_config'] = $element; + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $configuration = $form_state->getValue('workflow_transition_action_config'); + // Remove the transition: generates an error upon saving the action definition. + unset($configuration['workflow_transition']); + + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + $access = AccessResult::allowed(); + return $return_as_object ? $access : $access->isAllowed(); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Block/WorkflowTransitionBlock.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Block/WorkflowTransitionBlock.php new file mode 100644 index 000000000..158c0d4d7 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Block/WorkflowTransitionBlock.php @@ -0,0 +1,85 @@ + DRUPAL_NO_CACHE, // DRUPAL_CACHE_PER_ROLE will be assumed. + * + * @Block( + * id = "workflow_transition_form_block", + * admin_label = @Translation("Workflow Transition form"), + * category = @Translation("Forms") + * ) + */ +class WorkflowTransitionBlock extends BlockBase { + + /** + * {@inheritdoc} + */ + protected function blockAccess(AccountInterface $account) { + /* @var $entity \Drupal\Core\Entity\EntityInterface */ + if (!$entity = workflow_url_get_entity()) { + return AccessResult::forbidden(); + } + + // Only show block on entity view page (when default operation = ''). + if ($operation = workflow_url_get_operation()) { + return AccessResult::forbidden(); + } + + // Only show block if entity has workflow, and user has permission. + foreach (_workflow_info_fields($entity) as $definition) { + $type_id = $definition->getSetting('workflow_type'); + if ($account->hasPermission("access $type_id workflow_transition form")) { + return AccessResult::allowed(); + } + } + + return AccessResult::forbidden(); + } + + /** + * {@inheritdoc} + */ + public function build() { + $form = []; + + // Get the entity for this form. + /* @var $entity \Drupal\Core\Entity\EntityInterface */ + if (!$entity = workflow_url_get_entity()) { + return $form; + } + // Get the field name. Avoid error on Node Add page. + if (!$field_name = workflow_get_field_name($entity)) { + return $form; + } + + /* + * Output: generate the Transition Form. + */ + // Add the WorkflowTransitionForm to the page. + $form = WorkflowManager::getWorkflowTransitionForm($entity, $field_name); + + return $form; + } + + /** + * Retrieves the entity form builder. + * + * @return \Drupal\Core\Entity\EntityFormBuilderInterface + * The entity form builder. + */ + protected function entityFormBuilder() { + return \Drupal::getContainer()->get('entity.form_builder'); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldFormatter/WorkflowDefaultFormatter.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldFormatter/WorkflowDefaultFormatter.php new file mode 100644 index 000000000..3ca3bef37 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldFormatter/WorkflowDefaultFormatter.php @@ -0,0 +1,206 @@ +get('current_user'), + $container->get('entity.manager'), + $container->get('entity.form_builder') + ); + } + + /** + * Constructs a new WorkflowDefaultFormatter. + * + * @param string $plugin_id + * The plugin_id for the formatter. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to which the formatter is associated. + * @param array $settings + * The formatter settings. + * @param string $label + * The formatter label display setting. + * @param string $view_mode + * The view mode. + * @param array $third_party_settings + * Third party settings. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager + * The entity manager + * @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder + * The entity form builder. + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); + $this->viewBuilder = $entity_manager->getViewBuilder('workflow_transition'); + $this->storage = $entity_manager->getStorage('workflow_transition'); + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_manager; + $this->entityFormBuilder = $entity_form_builder; + } + + /** + * {@inheritdoc} + * + * N.B. A large part of this function is taken from CommentDefaultFormatter. + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $output = []; + + $field_name = $this->fieldDefinition->getName(); + $entity = $items->getEntity(); + $entity_type = $entity->getEntityTypeId(); + + $user = \Drupal::currentUser(); // @todo #2287057: OK? + // @todo: Perhaps global user is not always the correct user. + // E.g., on ScheduledTransition->execute()? But this function is mostly used in UI. + + $current_sid = WorkflowManager::getCurrentStateId($entity, $field_name); + /* @var $current_state WorkflowState */ + $current_state = WorkflowState::load($current_sid); + + // First compose the current value with the normal formatter from list.module. + $elements = workflow_state_formatter($entity, $field_name, $current_sid); + + // The state must not be deleted, or corrupted. + if (!$current_state) { + return $elements; + } + + // Check permission, so that even with state change rights, + // the form can be suppressed from the entity view (#1893724). + $type_id = $current_state->getWorkflowId(); + if (!\Drupal::currentUser()->hasPermission("access $type_id workflow_transition form")) { + return $elements; + } + + // Workflows are added to the search results and search index by + // workflow_node_update_index() instead of by this formatter, so don't + // return anything if the view mode is search_index or search_result. + if (in_array($this->viewMode, ['search_result', 'search_index'])) { + return $elements; + } + + if ($entity_type == 'comment') { + // No Workflow form allowed on a comment display. + // (Also, this avoids a lot of error messages.) + return $elements; + } + + // Only build form if user has possible target state(s). + if (!$current_state->showWidget($entity, $field_name, $user, FALSE)) { + return $elements; + } + + // Remove the default formatter. We are now building the widget. + $elements = []; + + // BEGIN Copy from CommentDefaultFormatter + $elements['#cache']['contexts'][] = 'user.permissions'; + // Add the WorkflowTransitionForm to the page. + $output['workflows'] = WorkflowManager::getWorkflowTransitionForm($entity, $field_name); + + // Only show the add workflow form if the user has permission. + $elements['#cache']['contexts'][] = 'user.roles'; + // Do not show the form for the print view mode. + $elements[] = $output + [ + '#workflow_type' => $this->getFieldSetting('workflow_type'), + '#workflow_display_mode' => $this->getFieldSetting('default_mode'), + 'workflows' => [], + ]; + // END Copy from CommentDefaultFormatter + + return $elements; + } + + /** + * Retrieves the entity form builder. + * + * @return \Drupal\Core\Entity\EntityFormBuilderInterface + * The entity form builder. + */ + protected function entityFormBuilder() { + if (!$this->entityFormBuilder) { + $this->entityFormBuilder = $this->container()->get('entity.form_builder'); + } + return $this->entityFormBuilder; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldType/WorkflowItem.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldType/WorkflowItem.php new file mode 100644 index 000000000..b07bea12f --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldType/WorkflowItem.php @@ -0,0 +1,421 @@ + label' pairs, i.e. 'Publishing': 1 => unpublished, 2 => draft, 3 => published."), + * category = @Translation("Workflow"), + * default_widget = "workflow_default", + * default_formatter = "list_default", + * constraints = { + * "WorkflowField" = {} + * }, + * ) + */ +class WorkflowItem extends ListItemBase { + //class WorkflowItem extends FieldItemBase implements OptionsProviderInterface { + // @todo D8-port: perhaps even: + //class WorkflowItem extends FieldStringItem { + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $schema = [ + 'columns' => [ + 'value' => [ + 'description' => 'The {workflow_states}.sid that this entity is currently in.', + 'type' => 'varchar', + 'length' => 128, + ], + ], + 'indexes' => [ + 'value' => ['value'], + ], + ]; + + return $schema; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + + /** + * Property definitions of the contained properties. + * + * @see FileItem::getPropertyDefinitions() + * + * @var array + */ + static $propertyDefinitions; + + + $definition['settings']['target_type'] = 'workflow_transition'; + // Definitions vary by entity type and bundle, so key them accordingly. + $key = $definition['settings']['target_type'] . ':'; + $key .= isset($definition['settings']['target_bundle']) ? $definition['settings']['target_bundle'] : ''; + + if (!isset($propertyDefinitions[$key])) { + + $propertyDefinitions[$key]['value'] = DataDefinition::create('string') // TODO D8-port : or 'any' + ->setLabel(t('Workflow state')) + ->addConstraint('Length', ['max' => 128]) + ->setRequired(TRUE); + + //workflow_debug( __FILE__ , __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + /* + // @todo D8-port: test this. + $propertyDefinitions[$key]['workflow_transition'] = DataDefinition::create('any') + // $properties['workflow_transition'] = DataDefinition::create('WorkflowTransition') + ->setLabel(t('Transition')) + ->setDescription(t('The computed WorkflowItem object.')) + ->setComputed(TRUE) + ->setClass('\Drupal\workflow\Entity\WorkflowTransition') + ->setSetting('date source', 'value'); + + $propertyDefinitions[$key]['display'] = array( + 'type' => 'boolean', + 'label' => t('Flag to control whether this file should be displayed when viewing content.'), + ); + $propertyDefinitions[$key]['description'] = array( + 'type' => 'string', + 'label' => t('A description of the file.'), + ); + + $propertyDefinitions[$key]['display'] = array( + 'type' => 'boolean', + 'label' => t('Flag to control whether this file should be displayed when viewing content.'), + ); + $propertyDefinitions[$key]['description'] = array( + 'type' => 'string', + 'label' => t('A description of the file.'), + ); + */ + } + return $propertyDefinitions[$key]; + } + + /** + * {@inheritdoc} + */ + public function getEntity() { + $entity = parent::getEntity(); + + // For Workflow on CommentForm, get the CommentedEntity. + if ($entity->getEntityTypeId() == 'comment') { + /** @var \Drupal\comment\CommentInterface $entity */ + $entity = $entity->getCommentedEntity(); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $is_empty = empty($this->value); + return $is_empty; + } + + /** + * {@inheritdoc} + */ + public function onChange($property_name, $notify = TRUE) { + //workflow_debug( __FILE__ , __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + + // TODO D8: use this function onChange for adding a line in table workflow_transition_* + // Enforce that the computed date is recalculated. + //if ($property_name == 'value') { + // $this->date = NULL; + //} + parent::onChange($property_name, $notify); + } + + /** + * {@inheritdoc} + */ + public static function defaultStorageSettings() { + + return [ + 'workflow_type' => '', + 'allowed_values_function' => 'workflow_state_allowed_values', + ] + parent::defaultStorageSettings(); + } + + /** + * Implements hook_field_settings_form() -> ConfigFieldItemInterface::settingsForm(). + */ + public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { + $element = []; + + // Create list of all Workflow types. Include an initial empty value. + // Validate each workflow, and generate a message if not complete. + $workflows = workflow_get_workflow_names(FALSE); + + // @todo D8: add this to WorkflowFieldConstraintValidator. + // Set message, if no 'validated' workflows exist. + if (count($workflows) == 1) { + drupal_set_message( + $this->t('You must create at least one workflow + before content can be assigned to a workflow.', + [':create' => Url::fromRoute('entity.workflow_type.collection')->toString(),] + ), 'warning' + ); + } + + // Validate via annotation WorkflowFieldConstraint. Show a message for each error. + $violation_list = $this->validate(); + /** @var \Symfony\Component\Validator\ConstraintViolation $violation */ + foreach ($violation_list->getIterator() as $violation) { + switch ($violation->getPropertyPath()) { + case 'fieldnameOnComment': + // @todo D8: CommentForm & constraints on storageSettingsForm() + // A 'comment' field name MUST be equal to content field name. + // @todo: Still not waterproof. You could have a field on a non-relevant entity_type. + drupal_set_message($violation->getMessage(), 'error'); + $workflows = []; + break; + + default: + break; + } + } + + // @todo D8: CommentForm & constraints on storageSettingsForm + // Set the required workflow_type on 'comment' fields. + // N.B. the following must BELOW the (count($workflows) == 1) snippet. + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage */ + $field_storage = $this->getFieldDefinition()->getFieldStorageDefinition(); + if (!$this->getSetting('workflow_type') && $field_storage->getTargetEntityTypeId() == 'comment') { + $field_name = $field_storage->get('field_name'); + $workflows = []; + foreach (_workflow_info_fields($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name) as $key => $info) { + if ($info->getName() == $field_name && ($info->getTargetEntityTypeId() !== 'comment')) { + $wid = $info->getSetting('workflow_type'); + $workflow = Workflow::load($wid); + $workflows[$wid] = $workflow->label(); + } + } + } + + // Let the user choose between the available workflow types. + $wid = $this->getSetting('workflow_type'); + $url = Url::fromRoute('entity.workflow_type.collection')->toString(); + $element['workflow_type'] = [ + '#type' => 'select', + '#title' => $this->t('Workflow type'), + '#options' => $workflows, + '#default_value' => $wid, + '#required' => TRUE, + '#disabled' => $has_data, + '#description' => $this->t('Choose the Workflow type. Maintain workflows + here.', [':url' => $url]), + ]; + + // Get a string representation to show all options. + + /* + * Overwrite ListItemBase::storageSettingsForm(). + */ + if ($wid) { + $allowed_values = WorkflowState::loadMultiple([], $wid); + // $allowed_values_function = $this->getSetting('allowed_values_function'); + + $element['allowed_values'] = [ + '#type' => 'textarea', + '#title' => $this->t('Allowed values for the selected Workflow type'), + '#default_value' => ($wid) ? $this->allowedValuesString($allowed_values) : [], + '#rows' => count($allowed_values), + '#access' => ($wid) ? TRUE : FALSE, // User can see the data, + '#disabled' => TRUE, // .. but cannot change them. + '#element_validate' => [[get_class($this), 'validateAllowedValues']], + + '#field_has_data' => $has_data, + '#field_name' => $this->getFieldDefinition()->getName(), + '#entity_type' => ($this->getEntity()) ? $this->getEntity()->getEntityTypeId() : '', + '#allowed_values' => $allowed_values, + '#description' => $this->allowedValuesDescription(), + ]; + } + + return $element; + } + + /** + * {@inheritdoc} + */ + protected function allowedValuesDescription() { + return ''; + } + + /** + * Generates a string representation of an array of 'allowed values'. + * + * This string format is suitable for edition in a textarea. + * + * @param WorkflowState[] $states + * An array of WorkflowStates, where array keys are values and array values are + * labels. + * + * @return string + * The string representation of the $states array: + * - Values are separated by a carriage return. + * - Each value is in the format "value|label" or "value". + */ + protected function allowedValuesString($states) { + $lines = []; + + $wid = $this->getSetting('workflow_type'); + + $previous_wid = -1; + /** @var \Drupal\workflow\Entity\WorkflowState $state */ + foreach ($states as $key => $state) { + // Only show enabled states. + if ($state->isActive()) { + // Show a Workflow name between Workflows, if more then 1 in the list. + if ((!$wid) && ($previous_wid <> $state->getWorkflowId())) { + $previous_wid = $state->getWorkflowId(); + $lines[] = $state->getWorkflow()->label() . "'s states: "; + } + $label = $this->t('@label', ['@label' => $state->label()]); + + $lines[] = " $key|$label"; + } + } + return implode("\n", $lines); + } + +// /** +// * {@inheritdoc} +// */ +// public static function generateSampleValue(FieldDefinitionInterface $field_definition) { +// // @todo Implement this once https://www.drupal.org/node/2238085 lands. +// $values['value'] = rand(pow(10, 8), pow(10, 9)-1); +// return $values; +// } + + + /** + * Implementation of TypedDataInterface + * + * @see folder \workflow\src\Plugin\Validation\Constraint + */ + + /** + * {@inheritdoc} + * + * @see folder \workflow\src\Plugin\Validation\Constraint + */ +// public function getConstraints() { +// $constraints = parent::getConstraints(); +// return $constraints; +// } + + /** + * {@inheritdoc} + * + * @see folder \workflow\src\Plugin\Validation\Constraint + */ +// public function validate() { +// $result = parent::validate(); +// return $result; +// } + + /** + * Implementation of OptionsProviderInterface + * + * An array of settable options for the object that may be used in an + * Options widget, usually when new data should be entered. It may either be + * a flat array of option labels keyed by values, or a two-dimensional array + * of option groups (array of flat option arrays, keyed by option group + * label). Note that labels should NOT be sanitized. + */ + + /** + * {@inheritdoc} + */ + public function getPossibleValues(AccountInterface $account = NULL) { + // Flatten options first, because SettableOptions may have 'group' arrays. + $flatten_options = OptGroup::flattenOptions($this->getPossibleOptions($account)); + return array_keys($flatten_options); + } + + /** + * {@inheritdoc} + */ + public function getPossibleOptions(AccountInterface $account = NULL) { + $allowed_options = []; + + // When we are initially on the Storage settings form, no wid is set, yet. + if (!$wid = $this->getSetting('workflow_type')) { + return $allowed_options; + } + + // Create an empty State. This triggers to show all possible states for the Workflow. + if ($workflow = Workflow::load($wid)) { + // There is no entity, E.g., on the Rules action "Set a data value". + $user = workflow_current_user($account); // @todo #2287057: OK? + /** @var \Drupal\workflow\Entity\WorkflowState $state */ + $state = WorkflowState::create(['wid' => $wid, 'id' => '']); + $allowed_options = $state->getOptions(NULL, '', $user, FALSE); + } + + return $allowed_options; + } + + /** + * {@inheritdoc} + */ + public function getSettableValues(AccountInterface $account = NULL) { + // Flatten options first, because SettableOptions may have 'group' arrays. + $flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account)); + return array_keys($flatten_options); + } + + /** + * {@inheritdoc} + */ + public function getSettableOptions(AccountInterface $account = NULL) { + $allowed_options = []; + + // When we are initially on the Storage settings form, no wid is set, yet. + if (!$wid = $this->getSetting('workflow_type')) { + return $allowed_options; + } + + // Use the 'allowed_values_function' to calculate the options. + $definition = $this->getFieldDefinition()->getFieldStorageDefinition(); + $entity = $this->getEntity(); + $field_name = $definition->getName(); + $user = workflow_current_user($account); // @todo #2287057: OK? + + // Get the allowed new states for the entity's current state. + // @todo D8-port: overwrite getValue(). + // $sid = $value = $this->getValue(); + /** @var \Drupal\workflow\Entity\WorkflowState $state */ + $sid = workflow_node_current_state($entity, $field_name); + $state = WorkflowState::load($sid); + $allowed_options = ($state) ? $state->getOptions($entity, $field_name, $user, FALSE) : []; + + return $allowed_options; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php new file mode 100644 index 000000000..3f13f5b71 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Field/FieldWidget/WorkflowDefaultWidget.php @@ -0,0 +1,233 @@ + array( + 'label' => t('Workflow'), + 'field types' => array('workflow'), + 'settings' => array( + 'fieldset' => 0, + 'name_as_title' => 1, + 'comment' => 1, + ), + ), + */ + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + * + * Be careful: Widget may be shown in very different places. Test carefully!! + * - On a entity add/edit page + * - On a entity preview page + * - On a entity view page + * - On a entity 'workflow history' tab + * - On a comment display, in the comment history + * - On a comment form, below the comment history + * + * @todo D8: change "array $items" to "FieldInterface $items" + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + + $wid = $this->getFieldSetting('workflow_type'); + /** @var $workflow Workflow */ + if (!$workflow = Workflow::load($wid)) { + // @todo: add error message. + return $element; + } + + if ($this->isDefaultValueWidget($form_state)) { + // On the Field settings page, User may not set a default value + // (this is done by the Workflow module). + return []; + } + + /** @var $item \Drupal\workflow\Plugin\Field\FieldType\WorkflowItem */ + $item = $items[$delta]; + /** @var \Drupal\field\Entity\FieldConfig $field_config */ + $field_config = $item->getFieldDefinition(); + /** @var $field_storage \Drupal\field\Entity\FieldStorageConfig */ + $field_storage = $field_config->getFieldStorageDefinition(); + + $entity = $item->getEntity(); + $field_name = $field_storage->get('field_name'); + + // Create a transition, to pass to the form. No need to use setValues(). + $from_sid = workflow_node_current_state($entity, $field_name); + /** @var $transition WorkflowTransition */ + $transition = WorkflowTransition::create([$from_sid, 'field_name' => $field_name]); + $transition->setTargetEntity($entity); + + // Here, on entity form, not the $element is added, but the entity form. + // Problem 1: adding the element, does not add added fields. + // Problem 2: adding the form, generates wrong UI. + // Problem 3: does not work on ScheduledTransition. + + // Step 1: use the Element. + $element['#default_value'] = $transition; + $element += WorkflowTransitionElement::transitionElement($element, $form_state, $form); + // Step 2: use the Form, in order to get extra fields. + $workflow_form = WorkflowManager::getWorkflowTransitionForm($entity, $field_name); + // Determine and add the attached fields. + $attached_fields = WorkflowManager::getAttachedFields('workflow_transition', $wid); + foreach ($attached_fields as $key => $attached_field) { + $element[$key] = $workflow_form[$key]; + } + + // Option 3: use the true Element. + // $form = $this->element($form, $form_state, $transition); + //$element['workflow_transition'] = array( + // '#type' => 'workflow_transition', + // '#title' => t('Workflow transition'), + // '#default_value' => $transition, + // ); + + return $element; + } + + /** + * {@inheritdoc} + * + * Implements workflow_transition() -> WorkflowDefaultWidget::submit(). + * + * Overrides submit(array $form, array &$form_state). + * Contains 2 extra parameters for D7 + * + * @param array $form + * @param array $form_state + * @param array $items + * The value of the field. + * @param bool $force + * TRUE if all access must be overridden, e.g., for Rules. + * + * @return int + * If update succeeded, the new State Id. Else, the old Id is returned. + * + * This is called from function _workflow_form_submit($form, &$form_state) + * It is a replacement of function workflow_transition($entity, $to_sid, $force, $field) + * It performs the following actions; + * - save a scheduled action + * - update history + * - restore the normal $items for the field. + * @todo: remove update of {node_form} table. (separate task, because it has features, too) + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + + $user = workflow_current_user(); // @todo #2287057: verify if submit() really is only used for UI. If not, $user must be passed. + + // Set the new value. + // Beware: We presume cardinality = 1 !! + // The widget form element type has transformed the value to a + // WorkflowTransition object at this point. We need to convert it + // back to the regular 'value' string format. + foreach ($values as &$item) { + if (!empty($item)) { // } && $item['value'] instanceof DrupalDateTime) { + + // The following can NOT be retrieved from the WorkflowTransition. + /** @var $entity \Drupal\Core\Entity\EntityInterface */ + $entity = $form_state->getFormObject()->getEntity(); + /** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */ + $transition = $item['workflow_transition']; + // N.B. Use a proprietary version of copyFormValuesToEntity, + // where $entity/$transition is passed by reference. + /** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */ + $transition = WorkflowTransitionElement::copyFormValuesToTransition($transition, $form, $form_state, $item); + + // Try to execute the transition. Return $from_sid when error. + if (!$transition) { + // This should not be possible (perhaps when testing/developing). + drupal_set_message(t('Error: the transition from %from_sid to %to_sid could not be generated.'), 'error'); + // The current value is still the previous state. + $to_sid = $from_sid = 0; + } + else { + // The transition may be scheduled or not. Save the result, and + // rely upon hook workflow_entity_insert/update($entity) in + // file workflow.module to save/execute the transition. + + // - validate option; add hook to let other modules change comment. + // - add to history; add to watchdog + // Return the new State ID. (Execution may fail and return the old Sid.) + + // Get the new value from an action button if set in the workflow settings. + $action_info = _workflow_transition_form_get_triggering_button($form_state); + $field_name = $transition->getFieldName(); + if ($field_name == $action_info['field_name']) { + $transition->set('to_sid', $action_info['to_sid']); + } + + $force = FALSE; // @TODO D8-port: add to form for usage in VBO. + + // Now, save/execute the transition. + $from_sid = $transition->getFromSid(); + $force = $force || $transition->isForced(); + + if (!$transition->isAllowed($user, $force)) { + // Transition is not allowed. + $to_sid = $from_sid; + } + elseif (!$entity || !$entity->id()) { + // Entity is inserted. The Id is not yet known. + // So we can't yet save the transition right now, but must rely on + // function/hook workflow_entity_insert($entity) in file workflow.module. + // $to_sid = $transition->execute($force); + $to_sid = $transition->getToSid(); + } + else { + // Entity is updated. To stay in sync with insert, we rely on + // function/hook workflow_entity_update($entity) in file workflow.module. + // $to_sid = $transition->execute($force); + $to_sid = $transition->getToSid(); + } + } + + // Now the data is captured in the Transition, and before calling the + // Execution, restore the default values for Workflow Field. + // For instance, workflow_rules evaluates this. + // + // N.B. Align the following functions: + // - WorkflowDefaultWidget::massageFormValues(); + // - WorkflowManager::executeTransition(). + // Set the transition back, to be used in hook_entity_update(). + $item['workflow_transition'] = $transition; + // Set the value at the proper location. + if ($transition && $transition->isScheduled()) { + $item['value'] = $from_sid; + } + else { + $item['value'] = $to_sid; + } + } + } + return $values; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/Validation/Constraint/WorkflowFieldConstraint.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/Validation/Constraint/WorkflowFieldConstraint.php new file mode 100644 index 000000000..dbc7eb15f --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/Validation/Constraint/WorkflowFieldConstraint.php @@ -0,0 +1,36 @@ +userStorage = $user_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('entity.manager')->getStorage('user')); + } + + /** + * {@inheritdoc} + */ + public function validate($entity, Constraint $constraint) { + // Workflow field name on CommentForm has special requirements. + $field_storage = $entity->getFieldDefinition()->getFieldStorageDefinition(); + if ($field_storage->getTargetEntityTypeId() == 'comment') { + if (!$this->isValidFieldname($field_storage)) { + $this->context->buildViolation($constraint->messageFieldname) + ->atPath('fieldnameOnComment') + ->addViolation(); + } + } + } + + /** + * @param FieldStorageConfig $field_storage + * + * @return bool + */ + protected function isValidFieldname(FieldStorageConfig $field_storage) { + $comment_field_name_ok = FALSE; + + if ($field_storage->getTargetEntityTypeId() !== 'comment') { + return TRUE; + } + + $field_name = $field_storage->get('field_name'); + + // Check if the 'comment' field name exists on the 'commented' entity type. + // @todo: Still not waterproof. You could have a field on a non-relevant entity_type. + foreach (_workflow_info_fields() as $key => $info) { + if (($info->getName() == $field_name) && ($info->getTargetEntityTypeId() !== 'comment')) { + $comment_field_name_ok = TRUE; + } + } + + return $comment_field_name_ok; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/Plugin/views/filter/WorkflowState.php b/sites/all/modules/contrib/admin/workflow/src/Plugin/views/filter/WorkflowState.php new file mode 100644 index 000000000..8556a69c2 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/Plugin/views/filter/WorkflowState.php @@ -0,0 +1,55 @@ +definition['wid']) ? $this->definition['wid'] : ''; + $grouped = isset($options['group_info']['widget']) ? $options['group_info']['widget'] == 'select' : false; + $this->valueOptions = workflow_get_workflow_state_names($wid, $grouped); + } + + /** + * Child classes should be used to override this function and set the + * 'value options', unless 'options callback' is defined as a valid function + * or static public method to generate these values. + * + * This can use a guard to be used to reduce database hits as much as + * possible. + * + * @return array|null + * The stored values from $this->valueOptions. + */ + public function getValueOptions() { + + if (isset($this->valueOptions)) { + return $this->valueOptions; + } + + //@todo: implement the below code, and remove the line from init. + // @todo: follow Options patterns + // @see callback_allowed_values_function() + // @see options_allowed_values() + + return parent::getValueOptions(); + } +} diff --git a/sites/all/modules/contrib/admin/workflow/src/WorkflowAccessControlHandler.php b/sites/all/modules/contrib/admin/workflow/src/WorkflowAccessControlHandler.php new file mode 100644 index 000000000..f8993b570 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/WorkflowAccessControlHandler.php @@ -0,0 +1,146 @@ +prepareUser($account); + + // This is only for Edit/Delete transition. For Add/create, use createAccess. + switch ($entity->getEntityTypeId()) { + case 'workflow_scheduled_transition': + switch ($operation) { + case 'update': + // This operation is not defined for Scheduled Transitions. + $result = AccessResult::forbidden(); + break; + case 'delete': + // This operation is not defined for Scheduled Transitions. + $result = AccessResult::forbidden(); + break; + case 'revert': + // This operation is not defined for Scheduled Transitions. + $result = AccessResult::forbidden(); + break; + default: + $result = parent::access($entity, $operation, $account, $return_as_object); + break; + } // End of switch ($operation). + + break; + + case 'workflow_transition': + /* @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */ + $transition = $entity; + switch ($operation) { + case 'update': + $is_owner = WorkflowManager::isOwner($account, $transition); + $type_id = $transition->getWorkflowId(); + if ($account->hasPermission("bypass $type_id workflow_transition access")) { + $result = AccessResult::allowed()->cachePerPermissions(); + } + elseif ($account->hasPermission("edit any $type_id workflow_transition")) { + $result = AccessResult::allowed()->cachePerPermissions(); + } + elseif ($is_owner && $account->hasPermission("edit own $type_id workflow_transition")) { + $result = AccessResult::allowed()->cachePerPermissions(); + } + return $return_as_object ? $result : $result->isAllowed(); + case 'delete': + // The delete operation is not defined for Transitions. + $result = AccessResult::forbidden(); + break; + case 'revert': + // @see workflow_operations. + default: + $result = parent::access($entity, $operation, $account, $return_as_object); + //if ($account->hasPermission("bypass $type_id workflow_transition access")) { + // $result = AccessResult::allowed()->cachePerPermissions(); + //} + break; + } // End of switch ($operation). + + break; + + case 'workflow_config_transition': + // This is not (yet) configured. + break; + + case 'workflow_state': + switch ($operation) { + case 'view label': + // The following two lines are copied from below, and need to be reviewed carefully. + $result = AccessResult::allowed(); + return $return_as_object ? $result : $result->isAllowed(); + default: + // E.g., operation 'update' on the WorkflowStates config page. + break; + } // End of switch ($operation). + + break; + + } + + /** @var $result AccessResult $result */ +// $result = parent::createAccess($entity_bundle, $account, $context, TRUE); + $result = $result->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + /** @var $result AccessResult $result */ + $result = parent::createAccess($entity_bundle, $account, $context, TRUE); + $result = $result->cachePerPermissions(); + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $transition, $operation, AccountInterface $account) { + return parent::checkAccess($transition, $operation, $account); + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + workflow_debug(__FILE__, __FUNCTION__, __LINE__); // @todo D8-port: still test this snippet. + return AccessResult::allowedIf($account->hasPermission('create ' . $entity_bundle . ' content'))->cachePerPermissions(); + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/WorkflowPermissions.php b/sites/all/modules/contrib/admin/workflow/src/WorkflowPermissions.php new file mode 100644 index 000000000..ab440c384 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/WorkflowPermissions.php @@ -0,0 +1,79 @@ +buildPermissions($type); + } + return $perms; + } + + /** + * Returns a list of workflow permissions for a given workflow type. + * + * @param \Drupal\workflow\Entity\Workflow $type + * The workflow type. + * + * @return array + * An associative array of permission names and descriptions. + */ + protected function buildPermissions(Workflow $type) { + $type_id = $type->id(); + $type_params = ['%type_name' => $type->label()]; + + return [ + // D7->D8-Conversion of the 'User 1 is special' permission (@see NodePermissions::bypass node access). + "bypass $type_id workflow_transition access" => [ + 'title' => $this->t('%type_name: Bypass transition access control', $type_params), + 'description' => $this->t('View, edit and delete all transitions regardless of permission restrictions.'), + 'restrict access' => TRUE, + ], + // D7->D8-Conversion of 'participate in workflow' permission to "create $type_id transition" (@see NodePermissions::create content). + "create $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Participate in workflow', $type_params), + 'description' => $this->t("Role is enabled to create state transitions. (Determines transition-specific permission on the workflow admin page. + Warning: For better control, uncheck the 'Authenticated user', and manage the permissions per separate role.)"), + ], + // D7->D8-Conversion of 'schedule workflow transitions' permission to "schedule $type_id transition" (@see NodePermissions::create content). + "schedule $type_id workflow_transition" => [ + 'title' => $this->t('%type_name: Schedule state transition', $type_params), + 'description' => $this->t('Role is enabled to schedule state transitions.'), + ], + // D7->D8-Conversion of 'workflow history' permission on Workflow settings to "access $type_id overview" (@see NodePermissions::access content overview). + "access own $type_id workflow_transion overview" => [ + 'title' => $this->t('%type_name: Access Workflow history tab of own content', $type_params), + 'description' => $this->t('Role is enabled to view the "Workflow state transition history" tab on own entity.'), + ], + "access any $type_id workflow_transion overview" => [ + 'title' => $this->t('%type_name: Access Workflow history tab of any content', $type_params), + 'description' => $this->t('Role is enabled to view the "Workflow state transition history" tab on any entity.'), + ], + // D7->D8-Conversion of 'show workflow transition form' permission. @see #1893724 + "access $type_id workflow_transition form" => [ + 'title' => $this->t('%type_name: Access the Workflow state transition form on entity view page', $type_params), + 'description' => $this->t('Role is enabled to view a "Workflow state transition" block/widget and add a state transition on the entity page.'), + ], + ]; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/WorkflowScheduledTransitionViewsData.php b/sites/all/modules/contrib/admin/workflow/src/WorkflowScheduledTransitionViewsData.php new file mode 100644 index 000000000..9319a5d17 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/WorkflowScheduledTransitionViewsData.php @@ -0,0 +1,22 @@ +workflow_entity; // N.B. This is a custom variable. + $entity_type = $entity->getEntityTypeId(); + $entity_id = $entity->id(); + $field_name = workflow_url_get_field_name(); + + // @todo D8-port: document $limit. Should be used in pager, not in load(). + $this->limit = \Drupal::config('workflow.settings')->get('workflow_states_per_page'); + $limit = $this->limit; + // Get Transitions with highest timestamp first. + $entities = WorkflowTransition::loadMultipleByProperties($entity_type, [$entity_id], [], $field_name, '', $limit, 'DESC'); + return $entities; + } + + /** + * {@inheritdoc} + * + * Building the header and content lines for the contact list. + * + * Calling the parent::buildHeader() adds a column for the possible actions + * and inserts the 'edit' and 'delete' links as defined for the entity type. + */ + public function buildHeader() { + + $entity = $this->workflow_entity; // N.B. This is a custom variable. + + $header['timestamp'] = $this->t('Date'); + if ($this->showColumnFieldname($entity)) { + $header['field_name'] = $this->t('Field name'); + } + $header['from_state'] = $this->t('From State'); + $header['to_state'] = $this->t('To State'); + $header['user_name'] = $this->t('By'); + $header['comment'] = $this->t('Comment'); + + // column 'Operations' is now added by core. + //$header['operations'] = $this->t('Operations'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $transition) { + // Show the history table. + $current_themed = FALSE; + /** @var $transition \Drupal\workflow\Entity\WorkflowTransitionInterface */ + $entity = $transition->getTargetEntity(); + $field_name = $transition->getFieldName(); + $current_sid = workflow_node_current_state($entity, $field_name); + + $to_state = $transition->getToState(); + if (!$to_state) { + // This is an invalid/deleted state. + $to_label = self::WORKFLOW_MARK_STATE_IS_DELETED; + // Add a footer to explain the addition. + $this->footer_needed = TRUE; + } + else { + $label = Html::escape($this->t($to_state->label())); + if ($transition->getToSid() == $current_sid && $to_state->isActive() && !$current_themed) { + $to_label = $label; + + // Make a note that we have themed the current state; other times in the history + // of this entity where the entity was in this state do not need to be specially themed. + $current_themed = TRUE; + } + elseif (!$to_state->isActive()) { + $to_label = $label . self::WORKFLOW_MARK_STATE_IS_DELETED; + // Add a footer to explain the addition. + $this->footer_needed = TRUE; + } + else { + // Regular state. + $to_label = $label; + } + } + unset($to_state); // Not needed anymore. + + $from_state = $transition->getFromState(); + if (!$from_state) { + // This is an invalid/deleted state. + $from_label = self::WORKFLOW_MARK_STATE_IS_DELETED; + // Add a footer to explain the addition. + $this->footer_needed = TRUE; + } + else { + $label = Html::escape($this->t($from_state->label())); + if (!$from_state->isActive()) { + $from_label = $label . self::WORKFLOW_MARK_STATE_IS_DELETED; + // Add a footer to explain the addition. + $this->footer_needed = TRUE; + } + else { + // Regular state. + $from_label = $label; + } + } + unset($from_state); // Not needed anymore. + + $owner = $transition->getOwner(); + $field_label = $transition->getFieldName(); + $variables = [ + 'transition' => $transition, + 'extra' => '', + 'from_label' => $from_label, + 'to_label' => $to_label, + 'user' => $owner, + ]; + // Allow other modules to modify the row. + \Drupal::moduleHandler()->alter('workflow_history', $variables); + + // 'class' => array('workflow_history_row'), // TODO D8-port + $row['timestamp']['data'] = $transition->getTimestampFormatted(); // 'class' => array('timestamp') + // html_entity_decode() transforms chars like '&' correctly. + if ($this->showColumnFieldname($entity)) { + $row['field_name']['data'] = html_entity_decode($field_label); + } + $row['from_state']['data'] = html_entity_decode($from_label); // 'class' => array('previous-state-name')) + $row['to_state']['data'] = html_entity_decode($to_label); // 'class' => array('state-name')) + $row['user_name']['data'] = $owner->toLink($owner->getDisplayName())->toString(); // 'class' => array('user-name') + $row['comment']['data'] = html_entity_decode($transition->getComment()); // 'class' => array('log-comment') + + // Column 'Operations' is now added by core. + // D7: $row['operations']['data'] = $this->buildOperations($entity); + $row += parent::buildRow($transition); + + return $row; + } + + /** + * {@inheritdoc} + * + * Builds the entity listing as renderable array for table.html.twig. + */ + public function render() { + $build = []; + + // @todo d8-port: get pager working. + $this->limit = \Drupal::config('workflow.settings')->get('workflow_states_per_page'); // TODO D8-port + // $output .= theme('pager', array('tags' => $limit)); // @todo D8-port + + $build += parent::render(); + + // Add a footer. This is not yet added in EntityListBuilder::render() + if ($this->footer_needed) { // @todo D8-port: test this. + // Two variants. First variant is official, but I like 2nd better. + /* + $build['table']['#footer'] = [ + [ + 'class' => ['footer-class'], + 'data' => [ + [ + 'data' => WORKFLOW_MARK_STATE_IS_DELETED . ' ' . t('State is no longer available.'), + 'colspan' => count($build['table']['#header']), + ], + ], + ], + ]; + */ + $build['workflow_footer'] = [ + '#markup' => WORKFLOW_MARK_STATE_IS_DELETED . ' ' . t('State is no longer available.'), + '#weight' => 500, // @todo Make this better. + ]; + } + + return $build; + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + + if (isset($operations['edit'])) { + $destination = \Drupal::destination()->getAsArray(); + $operations['edit']['query'] = $destination; + } + + return $operations; + } + + /** + * Gets the title of the page. + * + * @return string + * A string title of the page. + */ + protected function getTitle() { + return $this->t('Workflow history'); + } + + /** + * Determines if the column 'Field name' must be shown. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * + * @return bool + */ + protected function showColumnFieldname(EntityInterface $entity) { + if (is_null($this->show_column_fieldname)) { + // @todo: also remove when field_name is set in route?? + if (count(_workflow_info_fields($entity)) > 1) { + $this->show_column_fieldname = TRUE; + } + } + return $this->show_column_fieldname; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/src/WorkflowTransitionViewsData.php b/sites/all/modules/contrib/admin/workflow/src/WorkflowTransitionViewsData.php new file mode 100644 index 000000000..dff6d55d2 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/src/WorkflowTransitionViewsData.php @@ -0,0 +1,176 @@ +entityType->getBaseTable(); + $base_field = $this->entityType->getKey('id'); + + // @todo D8-port: Add data from D7 function workflow_views_views_data_alter() + // @see http://cgit.drupalcode.org/workflow/tree/workflow_views/workflow_views.views.inc + + $data[$base_table]['table']['group'] = $this->t('Workflow'); + $data[$base_table]['table']['provider'] = 'workflow'; + + $data[$base_table]['table']['join'] = [ + // This is provided for the many_to_one argument. + $base_table => [ + 'field' => $base_field, + 'left_field' => $base_field, + ], + ]; + + // @todo: this relationship needs to be reversed. See also taxonomy/src/NodeTermData.php + //$data[$base_table]['nid'] = [ + // 'title' => $this->t('Content with workflow'), + // 'help' => $this->t('Relate all content with a workflow.'), + // 'relationship' => [ + // 'id' => 'standard', + // 'base' => 'node', + // 'base field' => 'nid', + // 'label' => $this->t('node'), + // 'skip base' => 'node', + // ], + //]; + + $data[$base_table]['from_sid']['filter']['id'] = 'workflow_state'; + $data[$base_table]['from_sid']['help'] = $this->t('The name of the previous state of the transition.'); + + $data[$base_table]['to_sid']['filter']['id'] = 'workflow_state'; + $data[$base_table]['to_sid']['help'] = $this->t('The name of the new state of the transition. (For the latest transition, this is the current state.)'); + + $data[$base_table]['uid']['help'] = $this->t('The user who triggered the transition. If you need more fields than the uid add the content: author relationship'); + $data[$base_table]['uid']['filter']['id'] = 'user_name'; + $data[$base_table]['uid']['relationship']['title'] = $this->t('User'); + $data[$base_table]['uid']['relationship']['help'] = $this->t('The user who triggered the transition.'); + $data[$base_table]['uid']['relationship']['label'] = $this->t('User'); + + // @todo Add similar support to any date field + // @see https://www.drupal.org/node/2337507 + $data[$base_table]['timestamp_fulldate'] = [ + 'title' => $this->t('Created date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ + 'field' => 'timestamp', + 'id' => 'date_fulldate', + ], + ]; + + $data[$base_table]['timestamp_year_month'] = [ + 'title' => $this->t('Created year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ + 'field' => 'timestamp', + 'id' => 'date_year_month', + ], + ]; + + $data[$base_table]['timestamp_year'] = [ + 'title' => $this->t('Created year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ + 'field' => 'timestamp', + 'id' => 'date_year', + ], + ]; + + $data[$base_table]['timestamp_month'] = [ + 'title' => $this->t('Created month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ + 'field' => 'timestamp', + 'id' => 'date_month', + ], + ]; + + $data[$base_table]['timestamp_day'] = [ + 'title' => $this->t('Created day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ + 'field' => 'timestamp', + 'id' => 'date_day', + ], + ]; + + $data[$base_table]['timestamp_week'] = [ + 'title' => $this->t('Created week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ + 'field' => 'timestamp', + 'id' => 'date_week', + ], + ]; + + $data[$base_table]['changed_fulldate'] = [ + 'title' => $this->t('Updated date'), + 'help' => $this->t('Date in the form of CCYYMMDD.'), + 'argument' => [ + 'field' => 'changed', + 'id' => 'date_fulldate', + ], + ]; + + $data[$base_table]['changed_year_month'] = [ + 'title' => $this->t('Updated year + month'), + 'help' => $this->t('Date in the form of YYYYMM.'), + 'argument' => [ + 'field' => 'changed', + 'id' => 'date_year_month', + ], + ]; + + $data[$base_table]['changed_year'] = [ + 'title' => $this->t('Updated year'), + 'help' => $this->t('Date in the form of YYYY.'), + 'argument' => [ + 'field' => 'changed', + 'id' => 'date_year', + ], + ]; + + $data[$base_table]['changed_month'] = [ + 'title' => $this->t('Updated month'), + 'help' => $this->t('Date in the form of MM (01 - 12).'), + 'argument' => [ + 'field' => 'changed', + 'id' => 'date_month', + ], + ]; + + $data[$base_table]['changed_day'] = [ + 'title' => $this->t('Updated day'), + 'help' => $this->t('Date in the form of DD (01 - 31).'), + 'argument' => [ + 'field' => 'changed', + 'id' => 'date_day', + ], + ]; + + $data[$base_table]['changed_week'] = [ + 'title' => $this->t('Updated week'), + 'help' => $this->t('Date in the form of WW (01 - 53).'), + 'argument' => [ + 'field' => 'changed', + 'id' => 'date_week', + ], + ]; + + return $data; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/workflow.api.php b/sites/all/modules/contrib/admin/workflow/workflow.api.php new file mode 100644 index 000000000..c5da5f45f --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.api.php @@ -0,0 +1,431 @@ + $workflow->label()]); + $attributes = ['alt' => $alt, 'title' => $alt]; + $operations['workflow_access_form'] = [ + 'title' => t('Access'), + 'weight' => 50, + 'url' => \Drupal\Core\Url::fromRoute('entity.workflow_type.access_form', ['workflow_type' => $workflow->id()]), + 'query' => \Drupal::destination()->getAsArray(), // Add destination. + ]; + return $operations; + + case 'state': + /* @var $state WorkflowState */ + $state = $entity; + break; + + case 'workflow_transition': + // As of D8, below hook_workflow_operations is removed, in favour of core hooks. + // @see EntityListBuilder::getOperations, workflow_operations, workflow.api.php. + + // Your module may add operations to the Entity list. + /* @var $transition WorkflowTransitionInterface */ + $transition = $entity; + break; + + default: + break; + } + return $operations; +} + +/** + * Implements hook_workflow(). + * + * NOTE: This hook may reside in the implementing module + * or in a module.workflow.inc file. + * + * @param string $op + * The current workflow operation. + * E.g., 'transition pre', 'transition post'. + * @param WorkflowTransitionInterface $transition + * The transition, that contains all of the above. + * @param UserInterface $user + * + * @return bool|void + */ +function hook_workflow($op, WorkflowTransitionInterface $transition, UserInterface $user) { + switch ($op) { + case 'transition permitted': + // As of version 8.x-1.x, this operation is never called to check if transition is permitted. + // This was called in the following situations: + // case 1. when building a workflow widget with list of available transitions; + // case 2. when executing a transition, just before the 'transition pre'; + // case 3. when showing a 'revert state' link in a Views display. + // Your module's implementation may return FALSE here and disallow + // the execution, or avoid the presentation of the new State. + // This may be user-dependent. + // As of version 8.x-1.x: + // case 1: use hook_workflow_permitted_state_transitions_alter(); + // case 2: use the 'transition pre' operation; + // case 3: use the 'transition pre' operation; + return TRUE; + + case 'transition revert': + // Hook is called when showing the Transition Revert form. + // Implement this hook if you need to control this. + // If you return FALSE here, you will veto the transition. + + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, ''); + break; + + case 'transition pre': + // The workflow module does nothing during this operation. + // Implement this hook if you need to change/do something BEFORE anything + // is saved to the database. + // If you return FALSE here, you will veto the transition. + + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, ''); + break; + + case 'transition post': + // In D7, this is called by Workflow Node during update of the state, directly + // after updating the Workflow. Workflow Field does not call this, + // since you can call a hook_entity_* event after saving the entity. + // @see https://api.drupal.org/api/drupal/includes%21module.inc/group/hooks/7 + + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, $op, ''); + break; + + case 'transition delete': + case 'state delete': + case 'workflow delete': + // These hooks are removed in D8, in favour of the core hooks: + // - workflow_entity_predelete(EntityInterface $entity) + // - workflow_entity_delete(EntityInterface $entity) + // See examples at the bottom of this file. + break; + } + + return; +} + +/** + * Implements hook_workflow_history_alter(). + * + * In D8, hook_workflow_history_alter() is removed, in favour + * of ListBuilder::getDefaultOperations + * and hook_workflow_operations('workflow_transition'). + * + * Allow other modules to add Operations to the most recent history change. + * E.g., Workflow Revert implements an 'undo' operation. + * + * @param array $variables + * The current workflow history information as an array. + * 'old_sid' - The state ID of the previous state. + * 'old_state_name' - The state name of the previous state. + * 'sid' - The state ID of the current state. + * 'state_name' - The state name of the current state. + * 'history' - The row from the workflow_transition_history table. + * 'transition' - a WorkflowTransition object, containing all of the above. + */ +function hook_workflow_history_alter(array &$variables) { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); + + // The Workflow module does nothing with this hook. + // For an example implementation, see the Workflow Revert add-on. +} + +/** + * Implements hook_workflow_comment_alter(). + * + * Allow other modules to change the user comment when saving a state change. + * + * @param string $comment + * The comment of the current state transition. + * @param array $context + * 'transition' - The current transition itself. + */ +function hook_workflow_comment_alter(&$comment, array &$context) { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); + + /* @var $transition WorkflowTransitionInterface */ + $transition = $context['transition']; + //$comment = $transition->getOwner()->getUsername() . ' says: ' . $comment; +} + +/** + * Implements hook_workflow_permitted_state_transitions_alter(). + * + * @param array $transitions + * An array of allowed transitions from the current state (as provided in + * $context). They are already filtered by the settings in Admin UI. + * @param array $context + * An array of relevant objects. Currently: + * $context = array( + * 'user' => $user, + * 'workflow' => $workflow, + * 'state' => $current_state, + * 'force' => $force, + * ); + * + * This hook allows you to add custom filtering of allowed target states, add + * new custom states, change labels, etc. + * It is invoked in WorkflowState::getOptions(). + */ +function hook_workflow_permitted_state_transitions_alter(array &$transitions, array $context) { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); + + $user = $context['user']; // user may have the custom role AUTHOR. + // The following could be fetched from each transition. + $workflow = $context['workflow']; + $current_state = $context['state']; + // The following could be fetched from the $user and $transition objects. + $force = $context['force']; + + // Implement here own permission logic. + foreach ($transitions as $key => $transition) { + /* @var $transition WorkflowTransitionInterface */ + if (!$transition->isAllowed($user, $force)) { + //unset($transitions[$key]); + } + } + + // This example creates a new custom target state. + $values = [ + // Fixed values for new transition. + 'wid' => $context['workflow']->id(), + 'from_sid' => $context['state']->id(), + + // Custom values for new transition. + // The ID must be an integer, due to db-table constraints. + 'to_sid' => '998', + 'label' => 'go to my new fantasy state', + ]; + $new_transition = WorkflowConfigTransition::create($values); + // $transitions[] = $new_transition; +} + + +/********************************************************************** + * Hooks defined by core Form API: hooks to to alter the Workflow Form/Widget. + */ + +/** + * Alter forms for field widgets provided by other modules. + * + * @param $element + * The field widget form element as constructed by hook_field_widget_form(). + * @param $form_state + * An associative array containing the current state of the form. + * @param $context + * An associative array containing the following key-value pairs, matching the + * arguments received by hook_field_widget_form(): + * - form: The form structure to which widgets are being attached. This may be + * a full form structure, or a sub-element of a larger form. + * - field: The field structure. + * - instance: The field instance structure. + * - langcode: The language associated with $items. + * - items: Array of default values for this field. + * - delta: The order of this item in the array of subelements (0, 1, 2, etc). + * + * @see hook_field_widget_form() + * @see hook_field_widget_WIDGET_TYPE_form_alter() + */ +function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) { + // A hook for changing any widget. Better not use it: it is called on EVERY + // Widget. (Even though the message is only shown once.) + // D7: This hook is introduced in Drupal 7.8. + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); + // dpm($context['widget']->getPluginId()); +} + +function hook_field_widget_workflow_default_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) { + // A hook specific for the 'workflow_default' widget. + // D7: This hook is introduced in Drupal 7.8. + // D8: This name is specified in the annotation of WorkflowDefaultWidget. + workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', ''); + + // A widget on an entity form. + if ('workflow_default' == $context['widget']->getPluginId()) { + // This object contains all you need. You may find it in one of two locations. + /* @var $transition WorkflowTransitionInterface */ + /* @var $transition WorkflowTransitionInterface */ + $transition = $element['#default_value']; + + // An example of customizing/overriding the workflow widget. + // Beware, until now, you must do this twice: on the widget and on the form. + if ($transition->getOwnerId() == 1) { + drupal_set_message('(Test/Devel message) I got you, user 1, you will never schedule again, + and you WILL document each state change!', 'warning'); + // Let's prohibit scheduling for user 1. + $element['workflow_scheduling']['#access'] = FALSE; + // Let's prohibit scheduling for user 1. + if ($element['comment']['#access'] == TRUE) { + $element['comment']['#required'] = TRUE; + } + } + } +} + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + * + * Use this hook to alter the form. + * It is only suited if you only use View Page or Workflow Tab. + * If you change the state on the Entity Edit page (form), you need the hook + * hook_form_alter(). See below for more info. + */ +function hook_form_workflow_transition_form_alter(&$form, FormStateInterface $form_state, $form_id) { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, ''); + + // The WorkflowTransitionForm (E.g., Workflow History tab, Block). + // It has its own handling. + // @todo: Populate the WorkflowTransitionForm with a Widget, so we have 1 way-of-working. + + // Let's take a (changeable) reference to the element. + $workflow_element = &$form; + + // This object contains all you need. You may find it in one of two locations. + /* @var $transition WorkflowTransitionInterface */ + $transition = $form['workflow_transition']['#value']; + // dpm($transition); + + // An example of customizing/overriding the workflow widget. + // Beware, until now, you must do this twice: on the widget and on the form. + if ($transition->getOwnerId() == 1) { + drupal_set_message('(Test/Devel message) I got you, user 1, you will never schedule again, + and you WILL document each state change!', 'warning'); + // Let's prohibit scheduling for user 1. + $workflow_element['workflow_scheduling']['#access'] = FALSE; + // Let's prohibit scheduling for user 1. + if ( $workflow_element['comment']['#access'] == TRUE) { + $workflow_element['comment']['#required'] = TRUE; + } + } + + // Get the Entity. + /* @var $entity \Drupal\Core\Entity\EntityInterface */ + $entity = NULL; + //$entity = $form['workflow_entity']['#value']; + $entity_type = 'node'; // $form['workflow_entity_type']['#value']; + $entity_bundle = ''; // $entity->bundle(); + $sid = ''; + if ($entity) { + $entity_type = $entity->getEntityTypeId(); + $entity_bundle = $entity->bundle(); + + // Get the current State ID. + $sid = workflow_node_current_state($entity, $field_name = NULL); + // Get the State object, if needed. + $state = WorkflowState::load($sid); + } + + // Change the form, depending on the state ID. + // In the upcoming version 7.x-2.4, States should have a machine_name, too. + if ($entity_type == 'node' && $entity_bundle == 'MY_NODE_TYPE') { + switch ($sid) { + case '2': + // Change form element, form validate and form submit for state '2'. + break; + + case '3': + // Change form element, form validate and form submit for state '3'. + break; + } + } + +} + +/** + * Implements hook_form_alter(). + * + * Use this hook to alter the form on an Entity Form, Comment Form (Edit page). + * + * @see hook_form_workflow_transition_form_alter() for example code. + */ +function hook_form_alter(&$form, FormStateInterface $form_state, $form_id) { + if (substr($form_id, 0, 8) == 'workflow') { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, $form_id, ''); + } +} + + +/** + * Hooks defined by core Entity: hook_entity_CRUD. + * + * Instead of using hook_entity_OPERATION, better use hook_ENTITY_TYPE_OPERATION. + * + * @see hook_entity_create(), hook_entity_update(), etc. + * @see hook_ENTITY_TYPE_create(), hook_ENTITY_TYPE_update(), etc. + */ +function hook_entity_predelete(EntityInterface $entity) { + if (substr($entity->getEntityTypeId(), 0, 8) == 'workflow') { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'pre-delete' , $entity->getEntityTypeId()); + } + switch ($entity->getEntityTypeId()) { + case 'workflow_config_transition': + case 'workflow_state': + case 'workflow_type': + // Better use hook_ENTITY_TYPE_OPERATION. + // E.g., hook_workflow_type_predelete + break; + } +} + +function hook_entity_delete(EntityInterface $entity) { + if (substr($entity->getEntityTypeId(), 0, 8) == 'workflow') { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId()); + } +} + +function hook_workflow_type_delete(EntityInterface $entity) { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId()); +} + +function hook_workflow_config_transition_delete(EntityInterface $entity) { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId()); +} + +function hook_workflow_state_delete(EntityInterface $entity) { + // workflow_debug(__FILE__, __FUNCTION__, __LINE__, 'delete' , $entity->getEntityTypeId()); +} diff --git a/sites/all/modules/contrib/admin/workflow/workflow.field.inc b/sites/all/modules/contrib/admin/workflow/workflow.field.inc new file mode 100644 index 000000000..95e89fabb --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.field.inc @@ -0,0 +1,119 @@ +getFormObject()->getEntity()->getType(); + + if ($field_name == 'workflow') { + // Make sure only 1 value can be entered in the Workflow field. + $form['cardinality_container']['cardinality']['#default_value'] = 'number'; + $form['cardinality_container']['cardinality']['#disabled'] = TRUE; + $form['cardinality_container']['cardinality_number']['#default_value'] = 1; + $form['cardinality_container']['cardinality_number']['#disabled'] = TRUE; + $form['cardinality_container']['cardinality_number']['#states'] = []; + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function workflow_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $field_name = $form_state->getFormObject()->getEntity()->getType(); + + if ($field_name == 'workflow') { + // The Workflow field must have a value, so set to required. + $form['required']['#default_value'] = 1; + $form['required']['#disabled'] = TRUE; + + // There are alterations on the widget, too. + // @see WorkflowDefaultWidget::formElement(); + } +} + +/** + * We will be using some default formatters and widgets from the List and Options modules. + */ + +/** + * Implements hook_field_formatter_info_alter(). + * + * The module reuses the formatters defined in list.module. + */ +function workflow_field_formatter_info_alter(&$info) { + $info['list_key']['field_types'][] = 'workflow'; + $info['list_default']['field_types'][] = 'workflow'; +} + +/** + * Implements hook_field_widget_info_alter(). + * + * The module does not implement widgets of its own, but reuses the + * widgets defined in options.module. + * + * @see workflow_options_list() + */ +function workflow_field_widget_info_alter(&$info) { + $info['options_select']['field_types'][] = 'workflow'; + $info['options_buttons']['field_types'][] = 'workflow'; +} + +/** + * Creates a form element to show the current value of a Workflow state. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity this field is on. + * @param string $field_name + * The field_name + * @param string $current_sid + * The current State Id. + * + * @return array + * Form element, resembling the formatter of List module. + * If state 0 is given, return an empty form element. + */ +function workflow_state_formatter($entity, $field_name, $current_sid = '') { + $element = []; + + if (!$current_sid) { + $current_sid = workflow_node_current_state($entity, $field_name); + } + // If user creates a node, and only 1 option is available, the formatter + // is shown with key, not value, because creation state does not count. + // In this case, hide the formatter. + $state = \Drupal\workflow\Entity\WorkflowState::load($current_sid); + if ($state->isCreationState()) { + return $element; + } + // Clone the entity and restore old value, in case you want to show an + // executed transition. + if ($entity->$field_name->value != $current_sid) { + $entity = clone $entity; + $entity->$field_name->value = $current_sid; + } + // Generate a renderable array for the field. Use default language determination ($langcode = NULL). + // First, add the 'current value' formatter for this field. + // $list_display = $instance['display']['default']; + $list_display['type'] = 'list_default'; + $element = $entity->$field_name->view($list_display); + + // @todo D8: make weight better (even better: hook_field_extra_fields). + // Make sure the current value is before the form. (which has weight = 0.005) + // $element['#weight'] = 0; + + return $element; +} diff --git a/sites/all/modules/contrib/admin/workflow/workflow.form.inc b/sites/all/modules/contrib/admin/workflow/workflow.form.inc new file mode 100644 index 000000000..a26ed430c --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.form.inc @@ -0,0 +1,277 @@ +getBuildInfo()['base_form_id']) && $form_state->getBuildInfo()['base_form_id'] == 'workflow_transition_form') { + // The WorkflowTransitionForm::actions() has its own handling. + // E.g., Workflow History tab, Block. + return; + } + + // Find the first workflow. + // (So this won't work with multiple workflows per entity.) + $workflow_element = _workflow_transition_form_get_first_workflow_element($form); + + // Quit if there is no Workflow on this page. + if (!$workflow_element ) { + return; + } + + // Quit if there are no Workflow Action buttons. + // (If user has only 1 workflow option, there are no Action buttons.) + if (count($workflow_element['to_sid']['#options']) <= 1) { + return; + } + + // Find the default submit button and replace with our own action buttons. + if (isset($form['actions']['submit'])) { + $default_submit_action = $form['actions']['submit']; + // @todo D8-port: on Node-form, this is not sufficient. + unset($form['actions']['submit']); + } + elseif (isset($form['actions']['save'])) { + $default_submit_action = $form['actions']['save']; + unset($form['actions']['save']); + } + else { + // @todo test: when does this happen? + $default_submit_action = []; + } + if (isset($default_submit_action)) { + $actions = _workflow_transition_form_get_action_buttons($form, $workflow_element, $default_submit_action); + // Place the button with the other action buttons. + $form['actions'] = isset($form['actions']) ? $form['actions'] : []; + $form['actions'] += $actions; + } +} + +/** + * Fetches the first workflow_element from one of the Workflow Fields. + * + * @param array $form + * + * @return array + */ +function _workflow_transition_form_get_first_workflow_element(&$form) { + $workflow_element = []; + + // Find the first workflow. + // (So this won't work with multiple workflows per entity.) + foreach (\Drupal\Core\Render\Element::children($form) as $key) { + if (isset($form[$key]['widget'][0]['#default_value'])) { + $transition = $form[$key]['widget'][0]['#default_value']; + if (is_object($transition) && $transition instanceof WorkflowTransitionInterface ) { + $workflow_element = $form[$key]['widget'][0]; + break; + } + } + } + return $workflow_element; +} + +/** + * Returns the action buttons from the options widget. + * + * @param array $form + * @param array $workflow_element + * @param array $default_submit_action + * + * @return array $actions + */ +function _workflow_transition_form_get_action_buttons(array $form, array $workflow_element, array $default_submit_action) { + $actions = []; + $current_sid = $workflow_element['to_sid']['#default_value']; + /* @var $transition WorkflowTransitionInterface */ + $transition = $workflow_element['workflow_transition']['#value']; + $field_name = $transition->getFieldName(); + + // Find the default submit button and add our action buttons before it. + // Get the min weight for our buttons. + $option_weight = isset($default_submit_action['#weight']) ? $default_submit_action['#weight'] : 0; + $option_weight = $option_weight - count($workflow_element['to_sid']['#options']); + $min_weight = $option_weight; + foreach ($workflow_element['to_sid']['#options'] as $sid => $option_name) { + // Make the workflow button act exactly like the original submit button. + + $same_state_button = ($sid == $current_sid); + $workflow_submit_action = $default_submit_action; + // Add target State ID and Field name, to set correct value in validate_buttons callback. + $workflow_submit_action['#workflow'] = [ + 'field_name' => $field_name, + 'to_sid' => $sid, + ]; + // Keep option order. Put current state first. + $workflow_submit_action['#weight'] = ($same_state_button) ? $min_weight : ++$option_weight; + // Add/Overwrite some other settings. + $workflow_submit_action['#access'] = TRUE; + $workflow_submit_action['#value'] = $option_name; + // Use one drop button, instead of several action buttons. + if ('dropbutton' == _workflow_use_action_buttons()) { + $workflow_submit_action['#dropbutton'] = 'save'; + } + $workflow_submit_action['#attributes'] = ($same_state_button) ? ['class' => ['form-save-default-button']] : []; + $workflow_submit_action['#button_type'] = ($same_state_button) ? 'primary' : ''; // @todo: works for node form and workflow tab, not for workflow block. + //$workflow_submit_action['#executes_submit_callback'] = TRUE; + // Add class to workflow button. + $workflow_submit_action['#attributes']['class'][] = Html::getClass('workflow_button_' . $option_name); + // Append the form's #validate function, or it won't be called upon submit, + // because the workflow buttons have its own #validate. + $workflow_submit_action['#validate'] = []; + $workflow_submit_action['#validate'][] = '_workflow_transition_form_validate_buttons'; + if (isset($default_submit_action['#validate'])) { + $workflow_submit_action['#validate'] = $default_submit_action['#validate']; + } + elseif (isset($form['#validate'])) { + $workflow_submit_action['#validate'] = $form['#validate']; + } + // Append the submit-buttons's #submit function, or it won't be called upon submit. + if (isset($default_submit_action['#submit'])) { + $workflow_submit_action['#submit'] = $default_submit_action['#submit']; + } + elseif (isset($form['#submit'])) { + $workflow_submit_action['#submit'] = $form['#submit']; + } + + // Hide the same-state button in some cases. + if ($same_state_button) { + if (isset($form['#form_id']) && substr($form['#form_id'], 0, 24) == 'workflow_transition_form') { + // Hide same-state-button on the transition-form (that is: + // view page or workflow history tab) if there is nothing to do. + // However, a Transition may be fieldable. + if ($form['comment']['#access'] == FALSE) { + $workflow_submit_action['#access'] = FALSE; + } + } + elseif (isset($form['#id']) && $form['#id'] == 'comment-form') { + // On comment-form, the button must stay, since you can comment to same state. + } + else { + // On a entity edit page, the button must stay. + } + } + + // Place the button with the other action buttons. + $actions['workflow_' . $sid] = $workflow_submit_action; + } + + return $actions; +} + +/** + * Get the Workflow parameter from the button, pressed by the user. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @return array + * A $field_name => $to_sid array. + */ +function _workflow_transition_form_get_triggering_button(\Drupal\Core\Form\FormStateInterface $form_state) { + $result = ['field_name' => '', 'to_sid' => '']; + + $triggering_element = $form_state->getTriggeringElement(); + if (isset($triggering_element['#workflow'])) { + $result['field_name'] = $triggering_element['#workflow']['field_name']; + $result['to_sid'] = $triggering_element['#workflow']['to_sid']; + } + + return $result; +} + +/** + * Submit callback function for the Workflow Form / DefaultWidget. + * + * Validate form data for 'time' element. + * @param $element + * @param \Drupal\Core\Form\FormStateInterface $form_state + * @param $form + */ +function _workflow_transition_form_element_validate_time($element, \Drupal\Core\Form\FormStateInterface &$form_state, $form) { + if (!strtotime($element['#value'])) { + $form_state->setError($element, t('Please enter a valid value for time.')); + } +} + +/** + * Submit callback function for the Workflow Form / DefaultWidget. + * + * This is only used when using action buttons in the widget. + * It sets the new state to proper + * element and sets a submit function if needed, making sure the action is + * executed, influencing function core/includes/form.inc/form_execute_handlers(). + * (While constructing the Workflow form, we were not yet aware of the submit + * buttons of the complete form. We try to correct this here, without adding + * another hook_form_alter. We guess the first button is the Save button. + */ +function _workflow_transition_form_validate_buttons($form, \Drupal\Core\Form\FormStateInterface &$form_state) { + // Retrieve the data from the form. + $transition = $form_state->getValue('workflow_transition'); + if ($transition) { + // On WorkflowTransitionForm : + // D7: $form_state['input']['to_sid'] = $new_sid; + // D7: $form_state['values'][$field_name][$langcode][0]['to_sid'] = $new_sid; + + $values = $form_state->getValues(); + + $to_sid = $form_state->getTriggeringElement()['#workflow']['to_sid']; + $values['to_sid'] = $to_sid; + + // Update the form_state. + $form_state->setValues($values); + } + else { + // On edit form : See $form_state->getTriggeringElement() in WorkflowDefaultWidget; + } + +} diff --git a/sites/all/modules/contrib/admin/workflow/workflow.info.yml b/sites/all/modules/contrib/admin/workflow/workflow.info.yml new file mode 100644 index 000000000..0056bdfb2 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.info.yml @@ -0,0 +1,17 @@ +name: 'Workflow' +description: 'Defines a field type with Workflows, containing customizable state transitions.' +package: Workflow +type: module +# core: 8.x +# version: VERSION + +dependencies: + - field + - options + - user + +# Information added by Drupal.org packaging script on 2017-12-17 +version: '8.x-1.0' +core: '8.x' +project: 'workflow' +datestamp: 1513552400 diff --git a/sites/all/modules/contrib/admin/workflow/workflow.install b/sites/all/modules/contrib/admin/workflow/workflow.install new file mode 100644 index 000000000..0f1a48971 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.install @@ -0,0 +1,109 @@ + 'edit-modules-workflow']); + $message = t('Thanks for using Workflow. To maintain workflows, + install the Workflow UI module. To start using + a Workflow, add a Workflow Field to your entity.', + [':url' => $url->toString()] + ); + drupal_set_message($message); +} + +/** + * Implements hook_uninstall(). + */ +function workflow_uninstall() { +// \Drupal::config('workflow.settings')->clear('workflow_states_per_page')->save(); // @FIXME +} + +/** + * Implements hook_requirements(). + * + * Let admins know that Workflow is in use. + * + * @todo: extend workflow_requirements() for use with Workflow Field API. + * + * @param $phase + * @return array + */ +function workflow_requirements($phase) { + $requirements = []; + switch ($phase) { + case 'install': + break; + + case 'update': + break; + + case 'runtime': +// workflow_debug(__FILE__), __FUNCTION__, __LINE__, $form_id); // @todo D8-port: still test this snippet. + + /* + // Show info on admin/reports/status. + $type_list = 'Not yet determined.'; + + // $types = db_query('SELECT wid, type FROM {workflow_type_map} WHERE wid <> 0 ORDER BY type')->fetchAllKeyed(); + // If there are no types, then just bail. + if (count($types) == 0) { + return; + } + // Let's make it look nice. + if (count($types) == 1) { + $type_list = current($types); + } else { + $last = array_pop($types); + if (count($types) > 2) { + $type_list = implode(', ', $types) . ', and ' . $last; + } else { + $type_list = current($types) . ' and ' . $last; + } + } + */ + + /* + $requirements['workflow'] = array( + 'title' => $t('Workflow'), + 'value' => $t('Workflow is active on the @types content types.', array('@types' => $type_list)), + 'severity' => REQUIREMENT_OK, + ); + */ + break; + + } + + return $requirements; +} + +/** + * Implements hook_schema(). + * + * The D8-schema's have moved to: + * - Workflow annotation (it is a Config Entity now); + * - WorkfowState annotation (it is a Config Entity now); + * - WorkfowConfigTransition annotation (it is a Config Entity now); + * - WorkflowTransition::baseFieldDefinitions(); + * - WorkflowScheduledTransition::baseFieldDefinitions(). + */ +function workflow_schema() { + return $schema = []; +} + +/** + * Drupal 8 updates. + */ + +/** + * Update from version beta1 to beta2 are not possible. Please re-install this module. + */ +function workflow_update_8001(&$sandbox) { + drupal_set_message("Update from version beta1 to beta2 is not possible. Please re-install this module."); +} diff --git a/sites/all/modules/contrib/admin/workflow/workflow.links.task.yml b/sites/all/modules/contrib/admin/workflow/workflow.links.task.yml new file mode 100644 index 000000000..46ea56155 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.links.task.yml @@ -0,0 +1,32 @@ +#workflow.entities: +# class: \Drupal\Core\Menu\LocalTaskDefault +# deriver: \Drupal\workflow\Plugin\Derivative\WorkflowLocalTask + +#entity.workflow_type.canonical: +# route_name: entity.workflow_type.canonical +# base_route: entity.workflow_type.edit_form +# title: 'Edit' +# weight: -5 +entity.workflow_type.edit_form: + route_name: entity.workflow_type.canonical + base_route: entity.workflow_type.edit_form + title: 'Edit' + weight: -5 +entity.workflow_type.delete_form: + route_name: entity.workflow_type.delete_form + base_route: entity.workflow_type.edit_form + title: 'Delete' + weight: 10 + +# @todo D8-port: make Workflow History tab happen for every entity_type. +# @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController. +entity.node.workflow_history: + route_name: entity.node.workflow_history + base_route: entity.node.canonical + title: 'Workflow' + weight: 30 +entity.taxonomy_term.edit_form: + route_name: entity.taxonomy_term.workflow_history + base_route: entity.taxonomy_term.canonical + title: 'Workflow' + weight: 30 diff --git a/sites/all/modules/contrib/admin/workflow/workflow.module b/sites/all/modules/contrib/admin/workflow/workflow.module new file mode 100644 index 000000000..8e8ab2bc0 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.module @@ -0,0 +1,705 @@ +' . t('About') . ''; + $output .= '

' . t('The Workflow module adds a field to Entities to + store field values as Workflow states. You can control "state transitions" + and add action to specific transitions.') . '

'; + } + return $output; +} + +/** + * Implements hook_hook_info(). + * + * Allow adopters to place their hook implementations in either + * their main module or in a module.workflow.inc file. + */ +function workflow_hook_info() { + $hooks['workflow'] = ['group' => 'workflow']; + return $hooks; +} + +/********************************************************************** + * + * CRUD hooks. + * + */ + +/** + * Implements hook_user_cancel(). + * + * Update tables for deleted account, move account to user 0 (anon.) + * ALERT: This may cause previously non-Anonymous posts to suddenly + * be accessible to Anonymous. + * + * @see hook_user_cancel() + * + * @param $edit + * @param $account + * @param $method + */ +function workflow_user_cancel($edit, $account, $method) { + WorkflowManager::cancelUser($edit, $account, $method); +} + +/** + * Implements hook_user_delete(). + * @todo: hook_user_delete does not exist. hook_ENTITY_TYPE_delete? + * + * @param $account + */ +function workflow_user_delete($account) { + WorkflowManager::deleteUser($account); +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + * + * Is called when adding a new Workflow type. + * The technical name for the Workflow entity is 'workflow_type'. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + */ +function workflow_workflow_type_insert(\Drupal\Core\Entity\EntityInterface $entity) { + WorkflowManager::participateUserRoles($entity); +} + +/** + * Implements hook_entity_insert(). + * + * @param \Drupal\Core\Entity\EntityInterface $entity + */ +function workflow_entity_insert(\Drupal\Core\Entity\EntityInterface $entity) { + // Execute updates in hook_presave() to revert executions, + // Execute inserts in hook_insert, to have the Entity ID determined. + _workflow_execute_transitions($entity); +} + +/** + * Implements hook_entity_presave(). + * + * @param \Drupal\Core\Entity\EntityInterface $entity + */ +function workflow_entity_presave(\Drupal\Core\Entity\EntityInterface $entity) { + if (!$entity->isNew()) { + // Avoid a double call by hook_entity_presave and hook_entity_insert. + _workflow_execute_transitions($entity); + } +} + +/** + * Execute transitions. if prohibited, restore original field value. + * - insert: use hook_insert(), to have the Entity ID determined when saving transitions. + * - update: use hook_presave() to revert executions, + * - so, do not use hook_update(). + * + * @param \Drupal\Core\Entity\EntityInterface $entity + */ +function _workflow_execute_transitions(\Drupal\Core\Entity\EntityInterface $entity) { + // Avoid this hook on workflow objects. + if (WorkflowManager::isWorkflowEntityType($entity->getEntityTypeId())) { + return; + } + + // Execute/save the transitions fom the widgets in the entity form. + WorkflowManager::executeTransitionsOfEntity($entity); +} + +/** + * Implements hook_entity_delete(). + * + * Delete the corresponding workflow table records. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + */ +function workflow_entity_delete(\Drupal\Core\Entity\EntityInterface $entity) { + // @todo D8: test with multiple workflows. + if (get_class($entity) == 'Drupal\field\Entity\FieldConfig' + || get_class($entity) == 'Drupal\field\Entity\FieldStorageConfig') { + // A Workflow Field is removed from an entity. + $field_config = $entity; + /** @var \Drupal\Core\Entity\ContentEntityBase $field_config */ + $entity_type = $field_config->get('entity_type'); + $field_name = $field_config->get('field_name'); + /** @var $transition Drupal\workflow\Entity\WorkflowTransitionInterface */ + foreach (WorkflowScheduledTransition::loadMultipleByProperties($entity_type, [], [], $field_name) as $transition) { + $transition->delete(); + } + foreach (WorkflowTransition::loadMultipleByProperties($entity_type, [], [], $field_name) as $transition) { + $transition->delete(); + } + } + elseif (!WorkflowManager::isWorkflowEntityType($entity->getEntityTypeId())) { + // A 'normal' entity is deleted. + foreach ($fields = _workflow_info_fields($entity) as $field_id => $field_storage) { + $entity_type = $field_storage->getTargetEntityTypeId(); + $entity_id = $entity->id(); + $field_name = $field_storage->get('field_name'); + /** @var $transition Drupal\workflow\Entity\WorkflowTransitionInterface */ + foreach (WorkflowScheduledTransition::loadMultipleByProperties($entity_type, [$entity_id], [], $field_name) as $transition) { + $transition->delete(); + } + foreach (WorkflowTransition::loadMultipleByProperties($entity_type, [$entity_id], [], $field_name) as $transition) { + $transition->delete(); + } + } + } +} + +/** + * Implements hook_cron(). + * + * Given a time frame, execute all scheduled transitions. + */ +function workflow_cron() { + WorkflowManager::executeScheduledTransitionsBetween(0, \Drupal::time()->getRequestTime()); +} + +/** + * Business related functions, the API. + */ + +/** + * @deprecated D8: @see WorkflowManager::executeTransition(). + * + * @param \Drupal\workflow\Entity\WorkflowTransitionInterface $transition + * @param bool $force + * + * @return string + */ +function workflow_execute_transition(Drupal\workflow\Entity\WorkflowTransitionInterface $transition, $force = FALSE) { + // Execute transition and update the attached entity. + return $transition->executeAndUpdateEntity($force); +} + +/** + * Functions to get an options list (to show in a Widget). + * To be used in non-OO modules, like workflow_rules, workflow_views. + * + * The naming convention is workflow_get__names. + * (A bit different from 'user_role_names'.) + * Can be used for hook_allowed_values from list.module: + * - user_role + * - workflow + * - workflow_state + * - sid + */ + +/** + * Retrieves the names of roles matching specified conditions. + * + * deprecated D8: workflow_get_roles --> workflow_get_user_role_names + * + * Usage: + * D7: $roles = workflow_get_user_role_names('participate in workflow'); + * D8: $type_id = $workflow->id(); + * D8: $roles = workflow_get_user_role_names("create $type_id workflow_transition"); + * + * @param string $permission + * (optional) A string containing a permission. If set, only roles + * containing that permission are returned. Defaults to NULL, which + * returns all roles. + * Normal usage for filtering roles that are enabled in a workflow_type + * would be: $permission = 'create $type_id transition'. + * + * @return array + * Array of role names keyed by role ID, including the 'author' role. + */ +function workflow_get_user_role_names($permission) { + static $roles = NULL; + if ($roles[$permission]) { + return $roles[$permission]; + } + + // Copied from AccountForm::form(). + $roles[$permission] = array_map(['\Drupal\Component\Utility\Html', 'escape'], + [WORKFLOW_ROLE_AUTHOR_RID => '(' . t(WORKFLOW_ROLE_AUTHOR_NAME) . ')'] + + user_role_names(FALSE, $permission)); + + return $roles[$permission]; +} + +/** + * Get an options list for workflow states. + * + * @param mixed $wid + * The Workflow ID. + * @param bool $grouped + * Indicates if the value must be grouped per workflow. + * This influences the rendering of the select_list options. + * + * @return array + * An array of $sid => state->label(), grouped per Workflow. + */ +function workflow_get_workflow_state_names($wid = '', $grouped = FALSE) { + $options = []; + + // @todo: implement $add parameter. + // + // @todo: follow Options pattern + // @see callback_allowed_values_function() + // @see options_allowed_values() + + + // Get the (user-dependent) options. + // Since this function is only used in UI, it is save to use the global $user. + $user = workflow_current_user(); + + /** @var $workflows Workflow[] */ + $workflows = Workflow::loadMultiple($wid ? [$wid] : NULL); + // Do not group if only 1 Workflow is configured or selected. + $grouped = count($workflows) == 1 ? FALSE : $grouped; + + foreach ($workflows as $wid => $workflow) { + /** @var $state WorkflowState */ + $state = WorkflowState::create(['wid' => $wid]); + $workflow_options = $state->getOptions(NULL, '', $user, FALSE); + if (!$grouped) { + $options += $workflow_options; + } + else { + // Make a group for each Workflow. + $options[$workflow->label()] = $workflow_options; + } + } + + return $options; +} + +/** + * Get an options list for workflows. Include an initial empty value + * if requested. Validate each workflow, and generate a message if not complete. + * + * @param bool $required + * Indicates if the resulting list contains a options value. + * @return array + * An array of $wid => workflow->label(). + */ +function workflow_get_workflow_names($required = TRUE) { + $options = []; + + if (!$required) { + $options[''] = t('- Select a value -'); + } + foreach (Workflow::loadMultiple() as $wid => $workflow) { + /** @var $workflow Workflow */ + if ($workflow->isValid()) { + $options[$wid] = $workflow->label(); + } + } + + return $options; +} + +/** + * Gets an Options list of field names. + * + * @param null $entity + * @param string $entity_type + * @param string $entity_bundle + * @param string $field_name + * @return array + */ +function workflow_get_workflow_field_names($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name = '') { + $result = []; + foreach (_workflow_info_fields($entity, $entity_type, $entity_bundle, $field_name) as $definition) { + $field_name = $definition->getName(); + $result[$field_name] = $definition->getName(); + } + return $result; +} + +/** + * Helper function, to get the label of a given State Id. + * + * @param $sid + * + * @return string + * + * deprecated: workflow_get_sid_label() --> workflow_get_sid_name() + */ +function workflow_get_sid_name($sid) { + + if (empty($sid)) { + $label = 'No state'; + } + /** @noinspection PhpAssignmentInConditionInspection */ + elseif ($state = WorkflowState::load($sid)) { + $label = $state->label(); + } + else { + $label = 'Unknown state'; + } + return t($label); +} + +/** + * Determines the Workflow field_name of an entity. + * If an entity has multiple workflows, only returns the first one. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity at hand. + * @param string $field_name + * The field name. If given, will be passed as return value. + * + * @return string + */ +function workflow_get_field_name(\Drupal\Core\Entity\EntityInterface $entity, $field_name = '') { + if (!$entity) { + // $entity may be empty on Entity Add page. + return ''; + } + + if ($field_name) { + return $field_name; + } + + $fields = _workflow_info_fields($entity); + $field = reset($fields); + $field_name = $field->getName(); + return $field_name; +} + +/** + * Functions to get the state of an entity. + */ + +/** + * Wrapper function to get a UserInterface object. + * We use UserInterface to check permissions. + * + * @param \Drupal\Core\Session\AccountInterface|null $account + * + * @return \Drupal\user\UserInterface + */ +function workflow_current_user(\Drupal\Core\Session\AccountInterface $account = NULL) { + $account = ($account) ? $account : \Drupal::currentUser(); + return \Drupal\user\Entity\User::load($account->id()); +} + +/** + * Gets the current state ID of a given entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param string $field_name + * + * @return string $current_sid + * + * @deprecated : use WorkflowManager::getCurrentStateId() + */ +function workflow_node_current_state(\Drupal\Core\Entity\EntityInterface $entity, $field_name = '') { + return WorkflowManager::getCurrentStateId($entity, $field_name); +} + +/** + * Gets the previous state ID of a given entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param string $field_name + * + * @return string $previous_sid + * + * @deprecated : use WorkflowManager::getPreviousStateId() + */ +function workflow_node_previous_state(\Drupal\Core\Entity\EntityInterface $entity, $field_name = '') { + return WorkflowManager::getPreviousStateId($entity, $field_name); +} + +/** + * Get a specific workflow, given an entity type. Only one workflow is possible per node type. + * + * @param string $entity_bundle + * An entity bundle. + * @param string $entity_type + * An entity type. This is passed when also the Field API must be checked. + * + * @return Workflow + * A Workflow object, or NULL if no workflow is retrieved. + * + * Caveat: gives undefined results with multiple workflows per entity. + * + * @todo: support multiple workflows per entity. + */ +function workflow_get_workflows_by_type($entity_bundle, $entity_type) { + static $map = []; + + if (isset($map[$entity_type][$entity_bundle])) { + return $map[$entity_type][$entity_bundle]; + } + + $wid = FALSE; + if (isset($entity_type)) { + foreach (_workflow_info_fields(NULL, $entity_type, $entity_bundle) as $field_info) { + $wid = $field_info->getSetting('workflow_type'); + } + } + // Set the cache with a workflow object. + $map[$entity_type][$entity_bundle] = NULL; + if ($wid) { + $map[$entity_type][$entity_bundle] = Workflow::load($wid); + } + + return $map[$entity_type][$entity_bundle]; +} + +/** + * Gets the workflow field names, if not known already. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Object to work with. May be empty, e.g., on menu build. + * @param string $entity_type + * Entity type of object. Optional, but required if $entity provided. + * @param string $entity_bundle + * Bundle of entity. Optional. + * @param string $field_name + * Field name. Optional. + * + * @return Drupal\field\Entity\FieldStorageConfig[] + * An array of FieldStorageConfig objects. + */ +function _workflow_info_fields($entity = NULL, $entity_type = '', $entity_bundle = '', $field_name = '') { + $field_info = []; + + // Figure out the $entity's bundle and id. + if ($entity) { + $entity_type = $entity->getEntityTypeId(); + $entity_bundle = $entity->bundle(); + } + else { + // Entity type and bundle should be specified. + } + + $field_list = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('workflow'); + foreach ($field_list as $e_type => $data) { + if (!$entity_type || ($entity_type == $e_type)) { + foreach ($data as $f_name => $value) { + if (!$entity_bundle || isset($value['bundles'][$entity_bundle])) { + if (!$field_name || ($field_name == $f_name)) { + // Do not use the field_name as ID, but the unique . + // since you cannot share the same field on multiple entity_types (unlike D7). + $field_config = \Drupal\field\Entity\FieldStorageConfig::loadByName($e_type, $f_name); + $field_info[$field_config->id()] = $field_config; + } + } + } + } + } + return $field_info; +} + +/** + * Helper function to get the entity from a route. + * + * This is a hack. It should be solved by using $route_match. + * + * @param \Drupal\Core\Entity\EntityInterface|null $entity + * + * @return \Drupal\Core\Entity\EntityInterface + */ +function workflow_url_get_entity(\Drupal\Core\Entity\EntityInterface $entity = NULL) { + if ($entity) { + return $entity; + } + + $route_match = \Drupal::routeMatch(); + + // @todo: get entity for any route. + // On node pages, we'd get an object. + $entity = $route_match->getParameter('node'); + if ($entity && is_object($entity)) { + return $entity; + } + if ($entity && !is_object($entity)) { + // On workflow tab, we'd get an id. + $entity = \Drupal\node\Entity\Node::load($entity); + return $entity; + } + + // It was not a Node, try a Term. + // On term pages, we get objects, or id's. + $entity = $route_match->getParameter('taxonomy_term'); + if ($entity && is_object($entity)) { + return $entity; + } + elseif ($entity && !is_object($entity)) { + $entity = \Drupal\taxonomy\Entity\Term::load($entity); + return $entity; + } + + if (!$entity) { + // We may be on a entity add page/ + // Or we may be on a page of some unknown entity. + } + return $entity; +} + +/** + * Helper function to get the field name from a route. + * + * For now only used for ../{entity_id}/workflow history tab. + * + * @return string|null $field_name + */ +function workflow_url_get_field_name() { + /** @var $route_match \Drupal\Core\Routing\RouteMatchInterface */ + $route_match = \Drupal::routeMatch(); + $field_name = $route_match->getParameter('field_name'); + return $field_name; +} + +/** + * Helper function to get the entity from a route. + * + * @return mixed|string + */ +function workflow_url_get_operation() { + $url = \Drupal\Core\Url::fromRoute(''); + // The last part of the path is the operation: edit, workflow, devel. + $url_parts = explode('/', $url->toString()); + $operation = array_pop($url_parts); + // Except for view pages. + if (is_numeric($operation) || $operation == 'view') { + $operation = ''; + } + return $operation; +} + +/** + * Helper function to determine Workflow from Workflow UI URL. + * + * @param string $url + * @return Workflow + */ +function workflow_url_get_workflow($url = '' ) { + /** @var $workflows \Drupal\workflow\Entity\Workflow[] */ + static $workflows = []; + + // For some reason, $_SERVER is not allowed as default. + $url = ($url == '') ? $_SERVER['REQUEST_URI'] : $url; + + // The URL may have prefixes: / + // ex.1: /en/admin/config/workflow/workflow/MY_WORKFLOW/states + // ex.2: /admin/config/workflow/workflow/MY_WORKFLOW/states + $base_url = '/config/workflow/workflow/'; + $string = substr($url, strpos($url, $base_url) + strlen($base_url)); + $wid = explode('/', $string)[0]; + if (!isset($workflows[$wid])) { + $workflows[$wid] = Workflow::load($wid); + } + + return $workflows[$wid]; +} + + +/** + * Helper function to determine the title of the page. + * + * Used in file workflow_ui.routing.yml. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + */ +function workflow_url_get_title() { + $label = ''; + + // Get the Workflow from the page. + /** @var $workflow \Drupal\workflow\Entity\Workflow */ + /** @noinspection PhpAssignmentInConditionInspection */ + if ($workflow = workflow_url_get_workflow()) { + $label = $workflow->label(); + } + + $title = t('Edit @entity %label', ['@entity' => 'Workflow', '%label' => $label]); + return $title; +} + +/** + * Helper function to determine Workflow from Workflow UI URL. + * + * @param string $url + * @return mixed + */ +function workflow_url_get_form_type($url = '' ) { + // For some reason, $_SERVER is not allowed as default. + $url = ($url == '') ? $_SERVER['REQUEST_URI'] : $url; + + $base_url = '/config/workflow/workflow/'; + $string = substr($url, strpos($url, $base_url) + strlen($base_url)); + $type = explode('/', $string)[1]; + return $type; +} + +/** + * Helper function for D8-port: Get some info on screen. + * @see workflow_devel-module + * + * Usage: + * workflow_debug( __FILE__, __FUNCTION__, __LINE__, '', ''); // @todo D8-port: still test this snippet. + * + * @param string $class_name + * @param string $function_name + * @param string $line + * @param string $value1 + * @param string $value2 + */ +function workflow_debug($class_name, $function_name, $line = '', $value1 = '', $value2 = '') { + $debug_switch = FALSE; +// $debug_switch = TRUE; + + if (!$debug_switch) { + return; + } + + $class_name_elements = explode( "\\", $class_name); + $output = 'Testing... function ' . end($class_name_elements) . '::' . $function_name . '/' . $line; + if ($value1) { + $output .= ' = ' . $value1; + } + if ($value2) { + $output .= ' > ' . $value2; + } + drupal_set_message($output, 'warning', TRUE); +} diff --git a/sites/all/modules/contrib/admin/workflow/workflow.permissions.yml b/sites/all/modules/contrib/admin/workflow/workflow.permissions.yml new file mode 100644 index 000000000..83134605c --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.permissions.yml @@ -0,0 +1,10 @@ +# Permissions are declared in Workflow (not Workflow UI) so other contrib +# modules can make use of it. + +administer workflow: + title: 'Administer workflows' + description: 'Administer all Workflow configurations and settings.' + restrict access: true + +permission_callbacks: + - \Drupal\workflow\WorkflowPermissions::workflowTypePermissions diff --git a/sites/all/modules/contrib/admin/workflow/workflow.routing.yml b/sites/all/modules/contrib/admin/workflow/workflow.routing.yml new file mode 100644 index 000000000..2e124652f --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.routing.yml @@ -0,0 +1,144 @@ +# All other routes for Workflow maintenance are declared in Workflow UI. + +### Workflow CRUD +entity.workflow_type.collection: + path: '/admin/config/workflow/workflow' + defaults: + _entity_list: 'workflow_type' +# _controller: '\workflow\EntityWorkflowUIController::adminOverview' + _title: 'Workflows' + requirements: + _permission: 'administer workflow' + +entity.workflow_type.canonical: + path: '/admin/config/workflow/workflow/{workflow_type}' + defaults: + _entity_form: 'workflow_type.edit' + _title: 'Edit Workflow' + _title_callback: 'workflow_url_get_title' + requirements: + _permission: 'administer workflow' +# _entity_access: 'workflow.update' + +entity.workflow_type.add_form: + path: '/admin/config/workflow/workflow/add' + defaults: + _entity_form: 'workflow_type.add' + _title: 'Add Workflow' + requirements: + _permission: 'administer workflow' +# _entity_access: 'workflow.update' + +entity.workflow_type.edit_form: + path: '/admin/config/workflow/workflow/{workflow_type}' + defaults: + _entity_form: 'workflow_type.edit' + _title: 'Edit Workflow' + _title_callback: 'workflow_url_get_title' + requirements: + _permission: 'administer workflow' +# _entity_access: 'workflow.update' + +entity.workflow_type.delete_form: + path: '/admin/config/workflow/workflow/{workflow_type}/delete' + defaults: + _entity_form: 'workflow_type.delete' + _title: 'Delete Workflow' + requirements: + _permission: 'administer workflow' +# _entity_access: 'workflow.update' + +### Workflow Executed Transition CRUD +entity.workflow_transition.canonical: + path: '/workflow_transition/{workflow_transition}' + defaults: + # Calls the view controller, defined in the annotation of the contact entity + # _entity_view: 'workflow_transition' + _title: 'Workflow transition' + #requirements: + # Calls the access controller of the entity, $operation 'view' + # _entity_access: 'workflow_transition.view' + +entity.workflow_transition.edit_form: + path: '/workflow_transition/{workflow_transition}/edit' + defaults: + _entity_form: 'workflow_transition.edit' + _title: 'Edit Workflow transition' + options: + _admin_route: TRUE + requirements: + _entity_access: 'workflow_transition.update' + +entity.workflow_transition.delete_form: + path: '/workflow_transition/{workflow_transition}/delete' + defaults: + _entity_form: 'workflow_transition.delete' + _title: 'Delete Workflow transition' + options: + _admin_route: TRUE + requirements: + _entity_access: 'workflow_transition.delete' + +### Workflow Scheduled Transition CRUD +entity.workflow_scheduled_transition.canonical: + path: '/workflow_transition/{workflow_transition}' + defaults: + # Calls the view controller, defined in the annotation of the contact entity + # _entity_view: 'workflow_transition' + _title: 'Workflow transition' + #requirements: + # Calls the access controller of the entity, $operation 'view' + # _entity_access: 'workflow_transition.view' + +entity.workflow_scheduled_transition.edit_form: + path: '/workflow_transition/{workflow_transition}/edit' + defaults: + _entity_form: 'workflow_transition.edit' + _title: 'Edit Workflow transition' + options: + _admin_route: TRUE + requirements: + _entity_access: 'workflow_transition.update' + +entity.workflow_scheduled_transition.delete_form: + path: '/workflow_transition/{workflow_transition}/delete' + defaults: + _entity_form: 'workflow_transition.delete' + _title: 'Delete Workflow transition' + options: + _admin_route: TRUE + requirements: + _entity_access: 'workflow_transition.delete' + +### The Workflow Transition History List (Tab) +# @todo D8-port: make Workflow History tab happen for every entity_type. +# @see workflow.routing.yml, workflow.links.task.yml, WorkflowTransitionListController. +# A route for showing the Workflow history tab. +entity.node.workflow_history: + path: '/node/{node}/workflow/{field_name}' + defaults: + _title: 'Workflow history' + _controller: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyOverview' + field_name: ~ + requirements: + _custom_access: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyAccess' + _module_dependencies: 'node' + options: + _admin_route: TRUE + parameters: + entity: + type: entity:{entity_type} + +entity.taxonomy_term.workflow_history: + path: '/taxonomy/term/{taxonomy_term}/workflow/{field_name}' + defaults: + _title: 'Workflow history' + _controller: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyOverview' + field_name: ~ + requirements: + _custom_access: '\Drupal\workflow\Controller\WorkflowTransitionListController::historyAccess' + _module_dependencies: 'taxonomy' + options: + parameters: + entity: + type: entity:{entity_type} diff --git a/sites/all/modules/contrib/admin/workflow/workflow.services.yml b/sites/all/modules/contrib/admin/workflow/workflow.services.yml new file mode 100644 index 000000000..11489db5b --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.services.yml @@ -0,0 +1,10 @@ +services: + workflow.manager: + class: Drupal\workflow\Entity\WorkflowManager + arguments: ['@entity_type.manager', '@entity.query', '@config.factory', '@string_translation', '@module_handler', '@current_user'] + +# workflow.route_subscriber: +# class: Drupal\workflow\Routing\RouteSubscriber +# arguments: ['@entity.manager'] +# tags: +# - { name: event_subscriber } diff --git a/sites/all/modules/contrib/admin/workflow/workflow.views.inc b/sites/all/modules/contrib/admin/workflow/workflow.views.inc new file mode 100644 index 000000000..44acac408 --- /dev/null +++ b/sites/all/modules/contrib/admin/workflow/workflow.views.inc @@ -0,0 +1,78 @@ +getSettings(); + + foreach ($data as $table_name => $table_data) { + foreach ($table_data as $field_name => $field_data) { + if (isset($field_data['filter']) && $field_name != 'delta') { + $data[$table_name][$field_name]['filter']['wid'] = (array_key_exists('workflow_type', $settings)) ? $settings['workflow_type'] : ''; + $data[$table_name][$field_name]['filter']['id'] = 'workflow_state'; + } + } + } + + return $data; +} + +/** + * Implements hook_views_data_alter(). + */ +function workflow_views_data_alter(array &$data) { + // Provide an integration for each entity type except workflow entities. + // copied from comment.views.inc + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) { + if (\Drupal\workflow\Entity\WorkflowManager::isWorkflowEntityType($entity_type_id)) { + continue; + } + if (!$entity_type->entityClassImplements(\Drupal\Core\Entity\ContentEntityInterface::class)) { + continue; + } + if (!$entity_type->getBaseTable()) { + continue; + } + + $fields = \Drupal::service('workflow.manager')->getFields($entity_type_id); + if ($fields) { + $base_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); + $args = ['@entity_type' => $entity_type_id]; + foreach ($fields as $field_name => $field) { + $data[$base_table][$field_name . '_tid'] = [ + 'title' => t('Workflow transitions on @entity_type using field: @field_name', $args + ['@field_name' => $field_name]), + 'help' => t('Relate all transitions ongit status @entity_type. This will create 1 duplicate record for every transition. Usually if you need this it is better to create a Transition view.', $args), + 'relationship' => [ + 'group' => t('Workflow transition'), + 'label' => t('workflow transition'), + 'base' => 'workflow_transition_history', + 'base field' => 'entity_id', + 'relationship field' => $entity_type->getKey('id'), + 'id' => 'standard', + 'extra' => [ + [ + 'field' => 'entity_type', + 'value' => $entity_type_id, + ], + [ + 'field' => 'field_name', + 'value' => $field_name, + ], + ], + ], + ]; + } + } + } +} diff --git a/sites/default/config/sync/core.extension.yml b/sites/default/config/sync/core.extension.yml index 3a26930df..aa1a496fe 100644 --- a/sites/default/config/sync/core.extension.yml +++ b/sites/default/config/sync/core.extension.yml @@ -69,6 +69,7 @@ module: user: 0 views: 0 views_ui: 0 + workflow: 0 filefield_sources: 1 menu_admin_per_menu: 1 menu_link_content: 1 diff --git a/sites/default/config/sync/system.action.change_a_node_to_next_workflow_state.yml b/sites/default/config/sync/system.action.change_a_node_to_next_workflow_state.yml new file mode 100644 index 000000000..f64be1cd0 --- /dev/null +++ b/sites/default/config/sync/system.action.change_a_node_to_next_workflow_state.yml @@ -0,0 +1,25 @@ +uuid: 4fd977c2-10cc-46c8-b870-a375f4df2ac3 +langcode: nl +status: true +dependencies: + module: + - node + - workflow +_core: + default_config_hash: M98hvuYlwD42D6FTasB41hR2kTUpVtEB407qW4Ne1Ww +id: change_a_node_to_next_workflow_state +label: 'Change a node to next Workflow state' +type: node +plugin: workflow_node_next_state_action +configuration: + label: 'Change a node to next Workflow state' + id: change_a_node_to_next_workflow_state + plugin: workflow_node_next_state_action + type: node + workflow_scheduling: + scheduled: '0' + comment: 'New state is set by a triggered Action.' + force: 0 + actions: { } + field_name: '' + to_sid: '' diff --git a/sites/default/config/sync/views.view.workflow_entity_history.yml b/sites/default/config/sync/views.view.workflow_entity_history.yml new file mode 100644 index 000000000..e55baeefb --- /dev/null +++ b/sites/default/config/sync/views.view.workflow_entity_history.yml @@ -0,0 +1,716 @@ +uuid: 8f1952a2-102b-4873-ac33-1a585844b45a +langcode: fr +status: true +dependencies: + module: + - workflow +_core: + default_config_hash: WVVYWpOBeHeRgm3vCHP0lY65aX-UZx-E_8g6qpFrsHA +id: workflow_entity_history +label: 'Workflow Entity history' +module: views +description: 'Enable this View to configure the history tab.' +tag: '' +base_table: workflow_transition_history +base_field: hid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + 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: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + 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 + tags: + previous: ‹‹ + next: ›› + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + hid: hid + timestamp: timestamp + field_name: field_name + from_sid: from_sid + to_sid: to_sid + uid: uid + comment: comment + operations: operations + info: + hid: + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + timestamp: + align: '' + separator: '' + empty_column: false + responsive: '' + field_name: + align: '' + separator: '' + empty_column: false + responsive: '' + from_sid: + align: '' + separator: '' + empty_column: false + responsive: '' + to_sid: + align: '' + separator: '' + empty_column: false + responsive: '' + uid: + align: '' + separator: '' + empty_column: false + responsive: '' + comment: + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + align: '' + separator: '' + empty_column: false + responsive: '' + default: hid + empty_table: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + hid: + id: hid + table: workflow_transition_history + field: hid + relationship: none + group_type: group + admin_label: '' + label: 'Transition ID' + exclude: true + 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 + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + 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 + entity_type: workflow_transition + entity_field: hid + plugin_id: field + timestamp: + id: timestamp + table: workflow_transition_history + field: timestamp + relationship: none + group_type: group + admin_label: '' + label: Timestamp + 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 + click_sort_column: value + type: timestamp + settings: + date_format: medium + custom_date_format: '' + timezone: '' + 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 + entity_type: workflow_transition + entity_field: timestamp + plugin_id: field + field_name: + id: field_name + table: workflow_transition_history + field: field_name + relationship: none + group_type: group + admin_label: '' + label: 'Field name' + exclude: true + 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 + click_sort_column: value + type: string + settings: + link_to_entity: false + 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 + entity_type: workflow_transition + entity_field: field_name + plugin_id: field + from_sid: + id: from_sid + table: workflow_transition_history + field: from_sid + relationship: none + group_type: group + admin_label: '' + label: 'From state' + 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 + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + 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 + entity_type: workflow_transition + entity_field: from_sid + plugin_id: field + to_sid: + id: to_sid + table: workflow_transition_history + field: to_sid + relationship: none + group_type: group + admin_label: '' + label: 'To state' + 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 + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + 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 + entity_type: workflow_transition + entity_field: to_sid + plugin_id: field + uid: + id: uid + table: workflow_transition_history + field: uid + relationship: none + group_type: group + admin_label: '' + label: 'User ID' + 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 + click_sort_column: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + 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 + entity_type: workflow_transition + entity_field: uid + plugin_id: field + comment: + id: comment + table: workflow_transition_history + field: comment + relationship: none + group_type: group + admin_label: '' + label: 'Log message' + 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 + click_sort_column: value + type: basic_string + settings: { } + 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 + entity_type: workflow_transition + entity_field: comment + plugin_id: field + operations: + id: operations + table: workflow_transition_history + field: operations + relationship: none + group_type: group + admin_label: '' + label: 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 + destination: true + entity_type: null + entity_field: null + plugin_id: entity_operations + filters: { } + sorts: { } + header: { } + footer: { } + empty: { } + relationships: { } + arguments: + entity_id: + id: entity_id + table: workflow_transition_history + field: entity_id + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + not: false + entity_type: workflow_transition + entity_field: entity_id + plugin_id: numeric + display_extenders: { } + title: 'Workflow history' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + tags: { } + workflow_history_tab: + display_plugin: embed + id: workflow_history_tab + display_title: 'Workflow history tab' + position: 1 + display_options: + display_extenders: { } + display_description: 'Replaces the transition list in the Workflow tab.' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + tags: { } diff --git a/sites/default/config/sync/workflow.settings.yml b/sites/default/config/sync/workflow.settings.yml new file mode 100644 index 000000000..c498af684 --- /dev/null +++ b/sites/default/config/sync/workflow.settings.yml @@ -0,0 +1,4 @@ +workflow_states_per_page: 20 +_core: + default_config_hash: ne-hLr6xAfj-y7himkP4sQpLeAsTLAzt9YuxlQCk8N8 +langcode: fr