Closed (fixed)
Project:
Webform
Version:
8.x-5.x-dev
Component:
Code
Priority:
Normal
Category:
Feature request
Assigned:
Unassigned
Reporter:
Created:
6 Nov 2019 at 03:39 UTC
Updated:
2 Dec 2019 at 18:54 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
jrockowitz commentedAttached is a patch from #3089026: Add Group support to Webform access controls with
hook_webform_element_access().Comment #3
jrockowitz commentedComment #4
jrockowitz commentedI think the problem/challenge is
\Drupal\webform\Entity\Webform::checkElementsFlattenedAccesscalls\Drupal\webform\Plugin\WebformElementInterface::checkAccessRulesandWebform::checkElementsFlattenedAccesshas no awareness of the current webform submission.Webform::checkElementsFlattenedAccessis called the determine which elements should be available as columns in the Results table.Comment #5
jrockowitz commentedThe webform submission is stored via
$element['#webform_submission']by\Drupal\webform\Plugin\WebformElementBase::prepare. For the specific use case of controlling access to an element on the WebformSubmissionForm this might be okay. We might have to do something similar to the attached patch for certain calls to WebformElementInterface::checkAccessRules.Comment #6
jrockowitz commentedAttached are the two instances of calls to the WebformElementInterface::checkAccessRules 'view' operation which could easily include $element['#webform_submission']. I know this is a limited solution but I don't see it causing any regressions.
Comment #7
danchadwick commentedHere's a rough draft (i.e. it runs, but hasn't been tested) for your consideration. It makes the hook and webform handler element access handling be similar. If this is acceptable to you, I'll keep going. (To do: handle when submission is absent.)
Comment #8
jrockowitz commentedI much rather use
$element['#webform_submission']because it won't break anyone's existing code.Comment #9
danchadwick commentedI think the issue is
WebformElementBase::checkAccessRules(). It's unfortunately that PHP doesn't offer any way to distinguish "public" as in "I'm gonna use this in another class, but all you consumers of my code keep your mitts off" from "You should use this in your code". I very, very much doubt that any code calls checkAccessRules, but it certainly is theoretically possible.Solutions:
1) Pass the submission ID in the element and re-load the submission and webform in checkAccessRules. This would allow these to be passed to the hook and handler implementations. In 8.x-6.x, this could be cleaned up without having to change the signature of the hook and handler. But that is potentially a lot of calls to
Entity::load(), even though the actual entity will be cached, it's not nice for performance for a large webform.2) Pass the submission ID to the element and defer the re-load of the webform and submission to the hook and handler. This would be less computational work for when the webform and/or submission isn't needed, but it creates a weak signature that doesn't supply enough context to the hook and handler implementation. If this is fixed in 8.x-6.x, the hook and handlers would have to be updated.
3) Pass the webform and submission at the end of the signature to
checkAccessRules. This would allow other callers to use the old signature. The implementation could load the webform, but it would not be able to load the submission (since the caller would not have loaded it in#webform_submission.4) Leave the signature of
checkAccessRulesand document this in the release notes. The down side is that should anyone call this function, they would have to update before their code will run. OTOH, with solutions 1-3, they code will run but may not provide correct access checks because the code won't have the submission.5) Create a new function to replace
checkAccessRules()with the preferred signature, leaving the old version in place. Then rename the function and remove the old version in 8.x-6.x. This gives us signatures for the hooks and handlers which have context and won't break code (although it won't work correctly if the context is required by the site).I propose 5) with the namecheckAccessRulesWithContextto be renamed back to just checkAccessRules in 8.x-6.x.UPDATE: I think we should do 3) and revise the signature in 8.x-6.x with a change record to put the account back at the end where one typically finds it.
Comment #10
jrockowitz commentedI think we should do 1) first because the actual #webform and #webform_submission entity will be cached. Then we decide and figure out how to pass and reference the entities.
For example, it might make sense to add WebformElementInterface::setWebform and WebformElementInterface::getWebform with WebformElementInterface::setWebformSubmission and WebformElementInterface::getWebformSubmission methods. This approach would prevent passing $webform and $webform_submission between every method.
I ran into a similar issue with WebformHandlers and added a WebformHandler::setWebformSubmission method which solved a lot of problems.
Comment #11
jrockowitz commented@DanChadwick As I am working on this patch, I realize what a pickle of problem/challenge this issue is, thanks for the help.
Comment #12
jrockowitz commentedHere is the MVP patch and which still has the loading performance issue but all tests should be passing... I hope.
Comment #13
danchadwick commented@jrockowitz This is very inner-loop code. I stepped through
Entity::load()incheckAccessRules</code). Each <code>Entity::load()is 57 PHP debugger steps. So that 57 x 2 loads x #elements x #operations x (1 + #hooks or handlers that load too). For example, with 100 elements, view+update+delete, and one handler/hook, that's 68,400 unnecessary debugger steps. I'm not sure if this access is checked for reports, but that would be that x # rows in the report.In my mind it would be better to pass the webform and submission to the checkAccessRules and any hook / handler implementations to avoid all this unnecessary work. And a side benefit is that you are creating an API for the future that is easier to use and understand. Plus I think
Entity::load()will be removed in D9 (although webform could implement it for its entities).This can be done in a backwards-compatible way by passing webform and submission as new arguments after the account.
Your call of course. If you prefer to keep it as it, it looks good to me but I want write an implementation and test it.
By way of context (pun intended), the port of my webapp from D7 to D8 has yielded performance about 10x slower. I just ordered the fastest laptop I could find to make development less painful. I'm not sure how much caching is going to help me because the usage as an app is all authenticated and individual (people doing different things). It may be that I abandon this project after over a year of work and port D7 to backdrop. :(
Comment #14
jrockowitz commentedThe patch from #12 just gets the WebformHandler::accessElement working without breaking any tests. I agree that it is causing a major performance issue.
I am open to any help with performance improvements especially with Webform 8.x-6.x.
I think injecting the webform and submission into the WebformElement plugin is a more reliable and scalable solution than passing this information via a method. Once we have the webform and submission available in the WebformElement plugin we can pass it to $webform->invokeHandlers.
Comment #15
jrockowitz commentedThe attached patch shows how the webform/submission entity could be injected into the element plugin instance. This patched merge with #12 should reduce the number of calls when invoking check access.
Comment #16
jrockowitz commentedComment #17
danchadwick commentedThanks for this patch. Some thoughts:
1) There are 67 calls to getElementInstance. In each of these, the applicable context should be determined and passed. That's a pretty big set of changes, but I'd be happy to help if that's the way you want to go. The problem is that we can't be guaranteed that the proper context will be passed, which would lead to access (i.e. security) errors. Therefore the webform and submission context in the WebformElement probably cannot be relied upon since getElementInstance is public. If we required PHP 7.1, we could use a nullable type (...
?WebformInterface $webform...). This would at least cause runtime failures if someone outside of webform calls getElementInstance without any context. But I don't think we can require PHP 7.1. Poo.2) Minor point: I don't see any advantage in passing only one context variable to getElementInstance since it just then has to tease out the webform vs submission. I'd pass both and use the submission to find the webform if it is passed null. Also it provides better type checking on the paramenters.
3) Since we can't rely on the context in the WebformElement, we would have to manually set it using the two new setter methods immediately prior to calling checkAccessRules(). From a personal style perspective, I see this as more brittle than passing them arguments. It would be easy in the future to forget to set the context. Minor point though.
4) I still see no reason to not pass the context down to the hook implementation (but then I'm not using them so I don't really care).
5) For the handlers, it seems like the submission is set in the invokehandlers method. Is the webform guaranteed to always be set? I'm confused about why the submission is passed to the submission-related handler methods (e.g. postLoad, preSave, etc). If the handler can always get the webform and/or submission by its own getter, then certainly it doesn't have to be passed.
Not sure if I'm helping or hurting progress at this point. :)
Comment #18
jrockowitz commentedThe attached patch is #15 and #12 combined.
1) I am open to tweaking calls to ::getElementInstance().
2) I really wish PHP support method overloading.
3) I think we have to require the webform context in 8.x-6.x. One challenge is ::checkAccessRules() does not always have a submission context.
4) I am not sure about the hook. I am hesitant to have a hook with 5 parameters. We could add a $context parameter which contains the 'webform' and 'webform_submission'
5) My experience with trying to pass around the webform submission to the webform handler led me to implement the setter/getter Webform Submission . I ran into WebformHandlers needing to always be aware of the current webform submission. Personally, I like this pattern and could see isolating it to a trait and interface.
I am 100% willing to working with on improving the webform module's performance. On a related note, I found that xdebug slowed down my local Drupal site by 50/60% especially when running tests.
Comment #19
jrockowitz commentedComment #20
danchadwick commentedComments on #19
1)
WebformSubmission::checkAccessRulesI suggest passing$context = ['webform' => $webform, 'webform_submission' => $webform_submission]to webform_element_access hook implementations. Update the example in webform.api.php.2)
WebformSubmission::checkAccessRulesRemove&& $webform_submissionfrom code protecting invokeHandlers call. The handlers should be called if there is a webform but no submission. Not every handler implementation will need a submission.Also, wondering if it would be wise to throw an exception if $webform or $webform_submission is NULL but $element[[x] isn't empty. This would indicate an error condition and a possible security vulnerability.
3)
WebformElementBase::prepareUnless you are absolutely assured that $this had had its context set, call::setWebformSubmissionand maybe::setWebformbefore calling checkAcccessRules. I don't know enough about when prepare is called to know whether this is necessary.Otherwise, it looks good to me. Super job!
Comment #21
jrockowitz commented@DanChadwick I appreciate all your feedback. I will improve the APIs and explore handling some exceptions.
Comment #23
danchadwick commentedPhilosophical thought. I don't think it's super wonderful that the webform submission in the WebformElements is usually set but can't unequivocally be relied upon.
Ideally, I think the webform and webform_submission should be set when elements objects are created and updated when outdated (if that happens). Then you wouldn't have to set them before using them in checkAccessRules, the hooks, and the handlers. This would be something to consider for 8.x-6.x. For development purposes, you could throw an exception when the submission isn't what you expect it to be to help find issues.
I think this stems from the WebformElement objects not encapsulating the form elements because form elements are dumb arrays, rather than objects. I'm not sure this can be improved until core makes the rendering and FAPI more object oriented.
I'm happy to help more with this issue, but I haven't been very successful so far. We were working in parallel and you were faster than I. :)
Comment #25
jrockowitz commentedHere is my latest patch applying the comments from #20 with some exceptions and a dedicated \Drupal\webform\Plugin\WebformEntityInjectionTrait to establish the pattern.
Yes, we need to review the entire element initialize, prepare, finalize, render, etc... call stack and see if we can improve performance and data handling.
@see #1843798: [meta] Refactor Render API to be OO
Maybe there is a better way to handle FAPI element render array in the current OO WebformElement plugin.
Comment #26
danchadwick commentedLooking over #24:
-
WebformElementBase::checkAccessRulesThere is an inconsistency when no $webform context is present. If the element is private, it just returns false. If not, it throws an exception. I suggest a test for!$webformearly in the code and throw the exception there. As a plus, the code is shorter/cleaner.Patch attached.
Comment #27
danchadwick commentedI'm not sure how I indicate to apply #26 on the issue branch for testing.
Comment #28
jrockowitz commentedHere is my missing patch. I will work on applying the patch from #26 after all the test pass.
Comment #30
jrockowitz commentedHere is the patch with #26's change.
Comment #31
jrockowitz commentedSame patch minus a missing merge on my local branch.
Comment #32
danchadwick commentedI was surprised to have my
handler::accessElementcalled with an element that lacked a #webform_key. I see that this is used inWebformOtherBase::processWebformOther()to build the sub-elements of a select-other element.I'm don't see how
WebformElementBase::checkAccessRulescan have an opinion on access to these sub-elements since a) it lacks context to know what they are and b) they won't have any access rules. So I'm thinking that at the top, (right after the #access check) it should have:Without this, hook and handler implementations will be called on elements that they might not be expecting. And it's a tiny bit faster.
Am I wrong? Setting back to Needs Work pending your thoughts.
Comment #33
jrockowitz commented\Drupal\webform\Plugin\WebformElement\WebformCompositeBase::initializeCompositeElementsRecursive does set the '#webform_composite_id', '#webform_id', '#webform_composite_key', and '#webform_composite_parent_key' properties.
Currently, we are passing a composite's sub-element through accessing checking. I could see someone using #private in a composite sub-element.
Still, I would be open to blocking the new handler method and hook from sub-elements and see if someone asks for it to be enabled.
Comment #34
danchadwick commentedHow would #private get set on a sub-element? Through an alter webformhandler? Are these sub-elements used for view or does the main element view for them (in which case private wouldn't matter)?
If access control for sub-elements is a useful feature, then this could just be documented with a comment in WebformHandler::accessElement and the hook example in webform.api. Maybe something like "Note: This hook/handler may be called with any sub-elements used to implement a main element, in which case '#webform_key' will be absent from $element.
I don't have an opinion. I just didn't expect to be called with an element without a #webform_key.
Comment #36
jrockowitz commentedIn many cases, you can set sub-element property using '#SUB_ELEMENT__PROPERTY' (ie '#other__private': false).
The attached patch documents all the webform specific element #properties.
Comment #37
danchadwick commentedI see the new comments in webform.api. I didn't see anywhere that documents the ability to set '#SUBELEMENT__PROPERTY' or that accessElement should expect sub-elemens without a webform_key. Did I miss it or maybe you didn't intend to document these?
Aside from any desired additional docblock comments, I think this is RTBC.
Comment #39
jrockowitz commentedPatch now includes notes about #SUBELEMENT__PROPERTY.
Comment #42
danchadwick commentedCommit / revert. Problem?
Comment #43
jrockowitz commentedI ran into a hiccup when merging this with #3089026: Add Group support to Webform access controls. When hook_webform_element_access() returns neutral, access was denied to the element.
I couldn't figure out what was wrong so I reverted the patch to be safe.
Comment #44
danchadwick commentedI assume in webform group you deleted changed checkAccessRules and revised webform_group_webform_element_access() to return AccessResult?
Looking at the webform_group implementation, it should return return allowed where it returns true and neutral where it returns false or null.
Returning false in it's current implementation will be logically or'd with the result of checkAccessRule (by virtue of the array_sum). checkAccessRule never returns null.
I'm guessing that you had webform_group's hook returning forbidden, which is not at all what returning FALSE meant in the previous implementation in webform_group.
Suggest you apply this issue, then revert the changes in webform_group checkAccessRules, then in the hook implementation change TRUE to allowed and FALSE and NULL to neutral.
BTW, maybe you are aware of this, but the AccessResult rules are inconsistent and confusing. Entity access uses orIf() and this is consistent with the names allowed, neutral, and forbidden. However route access uses andIf() and in that case "neutral" really means "deny" and "forbidden" really means "super deny, even if you orIf() this result subsequently".
Comment #45
jrockowitz commented@DanChadwick Thank you for taking the time to summarize what most likely happened when I applied the patch.
I am attending a conference this week. I got nervous about the patch not working as expected and not being able to support it, which is why I reverted it.
I will apply the patch soon.
Comment #46
danchadwick commentedI took a stab in the webform group issue to merge webform group on top of this issue's #39. I have no idea why tests are failing.
Comment #47
jrockowitz commentedI can help move this forward next week.
Comment #50
jrockowitz commented