-
-
Notifications
You must be signed in to change notification settings - Fork 825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hooks/Dispatcher - Close loopholes that occur around "preboot" hooks #17831
Conversation
Before ------ TLDR: There are superfluous queries+hooks in every bootstrap. Specifically: * During early stages of bootstrap, the extension system loads the `CRM_Extension_Manager_*` classes. * The constructors for these classes lookup the option-group IDs in anticipation that they'll beeded. * The lookup uses a helper method which relies on `hook_entityTypes`. * Since all this happens during early bootstrap, `hook_entityTypes` becomes a preboot hook. * But it's silly - because we don't actually these groupIds. They're only used during installation/uninstallation of obscure extensions. After ----- TLDR: Less superfluous queries+hooks. * The `CRM_Extension_Manager_*` classes do not trigger any extraneous OG lookkups during bootstrap. * The `CRM_Extension_Manager_*` will only do the query when it really needs to. * `hook_entityTypes` fires later in bootstrap - when more services are available.
Before ------ * The event-dispatcher is fully instantiated as a container service (`Container::createEventDispatcher()`). * It is not possible to add listeners to `Civi::dispatcher()` before the container is instantiated. * It is not possible to add listeners to `Civi::dispatcher()` in the top-level logic of an extension file (`myext.php`) * There is a distinction between "pre-boot" hooks (`hook_civicrm_container` or `hook_civicrm_entityType`) and normal hooks. Pre-boot hooks do not work with `Civi::dispatcher()`. After ----- * The event-dispatcher is instantiated as boot service (ie very early during bootstrap). * To preserve compatibility (with `RegisterListenersPass`, with any downstream modifications of `createEventDispatcher`, and with any dependency-injection) the event-dispatcher will still use `createEventDispatcher()` for extra intialization. * It is possible to add listeners to `Civi::dispatcher()` in several more places, e.g. at the top-level of an extension's `.php` file and in `CRM_Utils_System_*::initialize()` * There is no distinction between "preboot" hooks and normal hooks. `Civi::dispatcher()` can register listeners for all hooks. * To ensure that we do not accidentally re-create "preboot" hooks, this patch enables a "dispatch policy" => if any hook fires too early, it will throw an exception.
(Standard links)
|
test this please |
@totten so I'm just looking at this from the point of view of whether it should get the 'has-test', 'ok-without-test' o 'needs-test' flag. It's definitely not in the first category. I think I apply the second when it either
This list probably can and should be expanded - & I'm hoping that if you make the case for 'ok -without-test' here it will help to clarify it |
Civi/Core/Container.php
Outdated
@@ -517,6 +520,8 @@ public static function boot($loadFromDB) { | |||
|
|||
$bootServices['lockManager'] = self::createLockManager(); | |||
|
|||
$bootServices['dispatcher.boot']->setDispatchPolicy(\CRM_Core_Config::isUpgradeMode() ? \CRM_Upgrade_DispatchPolicy::get('upgrade.main') : NULL); | |||
|
|||
if ($loadFromDB && $runtime->dsn) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eileenmcnaughton Yeah, good comment. It is fairly hard to frame a unit-test for this. It took me a bit to think up a QA/defensive mechanism. The primary risk, IMHO, is that some innocuous-looking patch will have some obscure side-effect which inadvertently causes some hook to dispatch in the midst of bootstrap.
(The stuff in a7718ac is a pretty good example of otherwise innocuous code which inadvertently dispatches hook_civicrm_entityTypes
.)
This patch offers a defense using setDispatchPolicy()
. To see this defense in action, you can sprinkle in calls to \CRM_Utils_Hook::entityTypes($x);
in various places (e.g. in createLockManager()
or SettingsManager::__construct()
or CRM_Utils_System_*::initialize()
) - if anything tries to fire a hook before the boot system is good-and-ready for it, then you'll get an exception.
(Clarification: The setDispatchPolicy()
notation is probably unfamiliar. The first policy ['/./' => 'not-ready']
matches the wildcard regex and throws a "Not ready" exception if any hooks are dispatched. The second policy is typically NULL
, which allows all hooks -- notwithstanding the pre-existing edge-case for upgrades.)
The key thing here is that the defensive measure is always used during boot()
. Consequently, every unit-test and every E2E test will employ this defensive measure. IMHO, the odds are quite high that some automated-test (if not all automated-tests) will raise a redflag on regression.
Arguably, the defense should be a little stronger - 42ccedc relaxes the dispatch-policy a little sooner than I'd like. It's just that the branch (if ($loadFromDB...)
means that the More Correct patch will also be More Ugly. But I suppose it's worth it - I can push up a tweak to tighten it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ab79d6c extends the defense a little further and expands the comments.
Before: The protection extends up until `createLockManager()` After: The protection extends up until `CRM_Extension_System...->register()`
ab79d6c
to
27e05c5
Compare
OK - I think we are homing in on dealing with this ongoing whack-a-mole - merging |
Overview
In
CRM_Utils_Hook
andCivi::dispatcher()
, there is a subtle distinction between normal hooks and "preboot" hooks. This eliminates the distinction, making it easier to reason about and modify the hook mechanism.Before
function PREFIX_civicrm_HOOK($param...)
orCivi::dispatcher()
.hook_civicrm_container
andhook_civicrm_entityTypes
), thenCivi::dispatcher()
does not work.Civi::dispatcher()
until fairly late during bootstrap.After
hook_civicrm_container
andhook_civicrm_entityTypes
should work withCivi::dispatcher()
.Civi::dispatcher()
), then it will raise an exception. This is not specifically a unit-test - but it would raised by the unit-tests for any use-cases involving a premature hook.Civi::dispatcher()
fairly early during bootstrap.Comments
In "Before/After", the third item speaks about "early" and "late" during bootstrap, and this is probably a bit abstract. My long-term aim is to move some of the civix boilerplate which will require other PR(s). For something immediate+concrete, this may be a better demonstration of the principle:
Civi::dispatcher()
must call it from within some other hook:Civi::dispatcher()
at the top-level of the PHP file:I haven't fully tested the lifecycle of an extension using this code-style (because it's a bit incidental/by-the-by), but it hopefully demonstrates the principle of the change.