Advanced Usage
For cPanel & WHM 11.32
Summary
The Standardized Hooks System offers more functionality than simply executing code. Information in the
Getting Started guide is a prerequisite to the following topics.
Rollbacks
A rollback is a way to undo changes made by the hook action code. If any hook action code returns a failure status, iteration of the main dispatch loop can be stopped, and an alternate loop initiated. This alternate dispatch loop iterates in reverse through the previous hooks and executes hook action code referenced in the
rollback descriptor, if the descriptor is defined.
Not all hookable events have access to the rollback feature. Only hookable events that are defined with the blocking attribute are valid candidates for this behavior. Providing a rollback is not required for qualifying hookable events; however, rollbacks can be quite valuable.
By definition, a blocking event has the ability to prevent the core cPanel & WHM event from occurring, potentially invalidating any work performed by the hook action code that ran during the dispatch loop.
Adding a Rollback
Defining a rollback is done in the same fashion as other descriptors. The rollback can be itemized in a describe pattern or can be specified using the CLI utility.
The code referenced by the rollback descriptor will be treated like the main hook action code:
- It receives the same context and dataset input arguments that were passed to the main hook action code.
- The check code should return a result status and optional result message in the same manner as the hook action code.
- Returned values, in list form for Perl modules, the first element being a boolean status and the second being the optional message string.
- Printed values to STDOUT for scripts, the leading character being 1 or 0 representing the boolean status, followed by a space and the optional message string.
The itemized rollback action is not required to be part of the hook action code base. It can simply be a reference to a completely different script or Perl module. However, it is required that the rollback action be in the same language as the main hook action code (i.e., either a Perl module the a script in the same language). Modules and scripts cannot be mixed within the same hook.
Examples
Perl module using the describe pattern subroutine
In the following example, if
Boo::InversePassword() successfully runs, but some other hook associated with the
pre stage of the
Passwd category and
ChangePasswd event fails, the Standardized Hooks System will perform
Boo::rollback_my_awesome_change().
sub describe {
my $hooks = [
{
'category' => 'Passwd',
'event' => 'ChangePasswd',
'stage' => 'pre',
'hook' => 'Boo::InversePassword',
'exectype' => 'module',
'rollback' => 'Boo::rollback_my_awesome_change',
},
];
return $hooks;
}
PHP script using descriptor options
In the following example, if
/var/cpanel/myapp/boo.php --inverse_password runs successfully, but some other hook associated with the
pre stage of the
Passwd category and
ChangePasswd event fails, the Standardized Hooks System will perform
/var/cpanel/myapp/boo.php --rollback_my_awesome_change.
[usr/local/cpanel/bin] $ ./manage_hooks add script /var/cpanel/myapp/boo.php --manual 1 --category Passwd --event ChangePasswd --stage pre --exectype script --action="--inverse_password" --rollback "/var/cpanel/myapp/boo.php --rollback_my_awesome_change"
Checks
Checks allow you to verify arbitrary conditions prior to performing the hook action code. This allows a developer to programmatically decide if it is valid for the itemized hook action code to run. If a check is specified in a Standardized Hook, the dispatch loop will execute the check reference. If the dispatch loop observes a success, then it will proceed to execute the main hook action code. If the loop observes a failure, the hook action code is skipped, as though it did not exist.
The code referenced by the check descriptor is treated like the main hook action code.
- It receives the same context and dataset input arguments that are passed to the main hook action code.
- The check code should return a result status and optional result message in the same manner as the hook action code.
- Returned values, in list form for Perl modules, the first element being a boolean status and the second being the optional message string.
- Printed values to STDOUT for scripts, the leading character being 1 or 0 representing the boolean status, followed by a space and the optional message string.
The itemized check action is not required to be part of the hook action code base. It can be a reference to a completely different script or Perl module. However, it is required that the check action be written in the same language as the main hook action code. (i.e., either a Perl module the a script in the same language)
Examples
Perl module using the describe pattern
In the following example, before a password change,
Boo::check_some_conditionals() will be executed. If that subroutine returns true, the Standardized Hooks System will perform
Boo::InversePassword().
sub describe {
my $hooks = [
{
'category' => 'Passwd',
'event' => 'ChangePasswd',
'stage' => 'pre',
'hook' => 'Boo::InversePassword',
'exectype' => 'module',
'check' => 'Boo::check_some_conditionals',
},
];
return $hooks;
}
PHP script using descriptor options
In the following example, before a password change,
/var/cpanel/myapp/boo.php --check_some_conditionals will be executed. If the script returns
1, the Standardized Hooks System will perform
/var/cpanel/myapp/boo.php --inverse_password.
[usr/local/cpanel/bin] $ ./manage_hooks add script /var/cpanel/myapp/boo.php --manual 1 --category Passwd --event ChangePasswd --stage pre --exectype script --action="--inverse_password" --check "/var/cpanel/myapp/boo.php --check_some_conditional"
Exceptions in hook action code
The use of
exceptions is commonplace for many developers. The Standardized Hooks System can honor exceptions that are raised by hook action code, as well as code executed as a part of a check action.
When an exception is raised by hook action code, or by the check action during a blocking event, the dispatch loop will halt iteration, log the exception to
/usr/local/cpanel/logs/error_log and proceed to iterate through any rollback actions.
When an exception is raised during a non-blocking event, the dispatch loop will log the exception, but continue to iterate through any remaining hook actions.
The following sections illustrate how to raise an exception that will print
Could not connect to remote server in the cPanel error log.
Examples
Raising exceptions in a Perl module
Any Perl function that normally raises an exception, like
die(), can be used. The Standardized Hooks System will know to catch this exception by it's message body. The message body must contain the term
BAILOUT.
# Perl Module Example
sub my_hook {
my ($context, $dataset) = @_;
my $conn = AwesomeServer->connect($user, $pass, $whatever);
if (!$conn) {
die "BAILOUT Could not connect to remote server";
}
#...
return 1;
}
Raising exceptions in a script
Different scripting languages have different implementations of exceptions. Therefore, to raise an exception in a script that will be caught by the Standardized Hooks System, the script must print
BAILOUT to STDOUT and exit.
// PHP Script Example
function my_hook($context, $dataset){
try {
$result = _hook($context, $dataset);
$msg = ($result)? "Hook succeeded" : "Hook failed";
echo "$result $msg";
}
catch(Exception $e) {
echo $e->getMessage();
exit;
}
}
function _hook($context, $dataset) {
my $conn = AwesomeServer->connect($user, $pass, $whatever);
if (!$conn) {
throw new RuntimeException("BAILOUT Could not connect to remote server");
}
#...
return 1;
}
Privilege Escalation
The Standardized Hooks System provides a privilege escalation path for hook action code authored as a script. If a given Standardized Hook defines the descriptor
escalateprivs as true, the system will execute the hook action code as the
root system user. Many hookable events already run as the
root user and will not require the
escalateprivs descriptor.
For information about which users run which hookable events, please review the
canonical hookable events list.
Defining the
escalateprivs descriptor is done in the same fashion as other descriptors of a Standardized Hook. It can be itemized in a describe pattern or added using the CLI management utility.
You should not use this feature lightly. If the hook action code can run as a user, it should do so by all means. Escalating privileges when it's not necessary can introduce undue risk to the system.
Examples
Perl script using a describe pattern
HTTP domain logs are parsed as the cPanel account that owns the domain. In this example, immediately prior to parsing HTTP domain logs as the cPanel user,
/var/cpanel/myapp/do_extra.pl will be executed as
root.
sub describe {
my $hooks = [
{
'category' => 'Stats',
'event' => 'RunUser',
'stage' => 'pre',
'hook' => '/var/cpanel/myapp/do_extra.pl’,
'exectype' => 'script',
'escalateprivs' => 1,
},
];
return $hooks;
}
PHP script using descriptor options
HTTP domain logs are parsed as the cPanel account that owns the respective domain. In this example, immediately prior to parsing HTTP domain logs as the cPanel user,
/var/cpanel/myapp/do_extra.php will be executed as
root.
./manage_hooks add script /var/cpanel/myapp/do_extra.php --manual --category Stats --event RunUser --stage pre --exectype script --escalateprivs
Defining hookable events in custom cPanel modules
The Standardized Hooks System does not restrict hookable events to the events listed in the
canonical reference. It is possible for non-cPanel code to contain arbitrary hookable events. In essence, a piece of non-cPanel code running as a cPanel process is treated as a native hookable event.
The minimum requirements for invoking hookable events in non-cPanel code are as follows:
- The calling code must be part of cPanel & WHM's runtime process, e.g., a cPanel or WHM plugin, child process of
cpsrvd, etc.
-
Note: It should be possible to leverage the Standardized Hooks System outside of cPanel processes; however, certain features, such as privilege escalation, will not work and will likely encounter errors. Generally speaking, we do not recommend using Standardized Hooks outside of a cPanel process.
- The calling code will need to be Perl and include the
Cpanel::Hooks module at /usr/local/cpanel/Cpanel/Hooks.pm.
We recommend that developers observe the following practices for consistency and expectations:
- In your custom code, bookend the primary logic with both a
pre and post stage. Most of the canonical category-events have these stages.
- For
pre stages, observe the return value and do not perform the primary logic. While the Standardized Hooks System will handle the rollback actions, it's up to the caller to not let the requested event occur.
- Let
post stages occur in a meaningful way with respect to the primary logic. Usually, this entails dispatching the post stage and providing a dataset that clearly shows if the primary logic completed successfully.
Example
package Cpanel::MyCustom;
package Cpanel::MyCustom;
use strict;
use warnings;
use Cpanel::Hooks ();
sub awesome {
my %OPTS = @_;
my $return_value = {
'status' => 0,
'status_msg' => undef
};
my ( $hook_result, $hook_msgs ) = Cpanel::Hooks::hook(
{
'category' => 'MyCustom',
'event' => 'awesome',
'stage' => 'pre',
'blocking' => 1,
},
\%OPTS,
);
my $awesome_data;
if ( !$hook_result ) {
my $hooks_msg = ( int @{$hook_msgs} ) ? join( "\n", @{$hook_msgs} ) : '';
$return_value->{'status_msg'} = 'Hook denied completion of '
."Cpanel::MyCustom::awesome(): $hooks_msg";
}
else {
# === do primary logic here === #
$awesome_data = {
'in_realworld' => 'this likely be the return a a subroutine',
'has_pass_criteria' => 'up to the subroutine that does the work',
};
# === set response accordingly === #
$return_value->{'data'} = $awesome_data;
if ( $awesome_data->{'has_pass_criteria'} ) {
$return_value->{'status'} = 1;
$return_value->{'status_msg'} = 'Successfully achieved awesomeness.';
}
else {
$return_value->{'status_msg'} = 'Failed to achieve awesomeness.';
}
}
Cpanel::Hooks::hook(
{
'category' => 'MyCustom',
'event' => 'awesome',
'stage' => 'post',
},
$return_value,
);
return $return_value;
}
1;