Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,47 @@ WP Crontrol is aware of timezones, will alert you to events that have no actions
1. Go to the `Tools → Cron Events` menu to manage cron events.
2. Go to the `Settings → Cron Schedules` menu to manage cron schedules.

## User Permissions

WP Crontrol uses fine-grained capabilities for managing different aspects of cron events:

* `view_cron_events` - View the list of cron events
* `edit_cron_event` - Edit existing cron events
* `delete_cron_event` - Delete cron events
* `create_cron_event` - Add new standard cron events
* `create_url_cron_event` - Add new URL cron events
* `create_php_cron_event` - Add new PHP cron events
* `run_cron_event` - Run cron events manually
* `pause_cron_event` - Pause or resume cron events
* `export_cron_events` - Export cron events

By default, all of these capabilities are granted to users with the `manage_options` capability (typically Administrators). The `create_php_cron_event` capability requires the `edit_files` capability instead.

### Customizing Capabilities

Developers can use the `user_has_cap` filter to customize these capabilities. For example, to allow only Network Admins to manage cron events on a multisite installation:

```php
add_filter( 'user_has_cap', function( $user_caps, $required_caps, $args, $user ) {
$cron_caps = [
'view_cron_events',
'edit_cron_event',
'delete_cron_event',
'create_cron_event',
'create_url_cron_event',
'run_cron_event',
'pause_cron_event',
'export_cron_events'
];

if ( in_array( $args[0], $cron_caps, true ) && user_can( $user, 'manage_network_options' ) ) {
$user_caps[ $args[0] ] = true;
}

return $user_caps;
}, 10, 4 );
```

## Documentation

[Extensive documentation on how to use WP Crontrol and how to get help for error messages that it shows is available on the WP Crontrol website](https://wp-crontrol.com/docs/how-to-use/).
Expand Down
2 changes: 2 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ Creating, editing, and running PHP cron events is subject to restrictive securit

Only users with the `manage_options` capability can manage cron events and schedules. By default, only Administrators have this capability.

WP Crontrol uses fine-grained capabilities for controlling access to specific actions such as editing and deleting cron events. [You can read all about these fine-grained capabilities here](https://wp-crontrol.com/docs/capabilities/).

### Which users can manage PHP cron events? Is this dangerous?

Only users with the `edit_files` capability can manage PHP cron events. This means if a user cannot edit files via the WordPress admin area (i.e. through the Plugin Editor or Theme Editor) then they also cannot add, edit, or delete a PHP cron event in WP Crontrol. By default only Administrators have this capability, and with Multisite enabled only Super Admins have this capability.
Expand Down
121 changes: 106 additions & 15 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
add_action( 'activated_plugin', __NAMESPACE__ . '\flush_status_cache', 10, 0 );
add_action( 'deactivated_plugin', __NAMESPACE__ . '\flush_status_cache', 10, 0 );
add_action( 'switch_theme', __NAMESPACE__ . '\flush_status_cache', 10, 0 );
add_filter( 'map_meta_cap', __NAMESPACE__ . '\filter_map_meta_cap', 10, 4 );
add_filter( 'user_has_cap', __NAMESPACE__ . '\filter_user_has_cap', 10, 4 );
}

/**
Expand Down Expand Up @@ -91,6 +93,92 @@
return get_transient( $key );
}

// @TODO check requirements:
// - allow a plugin to grant a cap to a lower level user, eg grant view_cron_events to editors
// - either by directly granting the cap, or by filtering `map_meta_cap` (to change required primitive cap) or `user_has_cap` (to grant or deny via its own logic)

Check failure on line 98 in src/bootstrap.php

View workflow job for this annotation

GitHub Actions / PHP / 8.3

Expected 1 space before comment text but found 3; use block comment if you need indentation
// - allow a plugin to deny a cap to a user, eg deny create_cron_events from admin or deny all cron caps from users who aren't super admins on multisite

/**
* Filters the primitive capabilities required based on the requested meta capability.
*
* @param string[] $required_caps Primitive capabilities required of the user.
* @param string $cap Capability being checked.
* @param int $user_id The user ID.
* @param mixed[] $args Adds context to the capability check, typically starting with an object ID.
* @return string[] Primitive capabilities required of the user.
*/
function filter_map_meta_cap( array $required_caps, $cap, $user_id, array $args ) {
switch ( $cap ) {
case 'view_cron_events':
case 'create_cron_events':
case 'create_url_cron_events':
case 'edit_cron_event':
case 'delete_cron_event':
case 'run_cron_event':
case 'pause_cron_event':
case 'resume_cron_event':
case 'export_cron_events':
$required_caps[] = 'manage_options';
break;
case 'create_php_cron_events':
$required_caps[] = 'edit_files';
break;
// case 'create_php_cron_events':
case 'edit_php_cron_event':
case 'delete_php_cron_event':
// PHP cron events require both edit_files and the meta capability
if ( ! php_cron_events_enabled() ) {
$required_caps[] = 'do_not_allow';
}
break;
}

return $required_caps;
}

/**
* Filters a user's capabilities to grant them WP Crontrol's meta capabilities.
*
* @param bool[] $user_caps Array of key/value pairs where keys represent a capability name and boolean values
* represent whether the user has that capability.
* @param string[] $required_caps Required primitive capabilities for the requested capability.
* @param mixed[] $args {
* Arguments that accompany the requested capability check.
*
* @type string $0 Requested capability.
* @type int $1 Concerned user ID.
* @type mixed ...$2 Optional second and further parameters.
* }
* @param \WP_User $user Concerned user object.
* @return bool[] Array of concerned user's capabilities.
*/
function filter_user_has_cap( array $user_caps, array $required_caps, array $args, \WP_User $user ) {
// Our meta capabilities and their default mappings to primitive capabilities
$meta_caps = array(
'view_cron_events' => 'manage_options',
'edit_cron_event' => 'manage_options',
'delete_cron_event' => 'manage_options',
'create_cron_event' => 'manage_options',
'create_url_cron_event' => 'manage_options',
'create_php_cron_event' => 'edit_files',
'run_cron_event' => 'manage_options',
'pause_cron_event' => 'manage_options',
'export_cron_events' => 'manage_options',
);

// Check if the requested capability is one of our meta capabilities
if ( isset( $meta_caps[ $args[0] ] ) ) {
$required_cap = $meta_caps[ $args[0] ];

// Grant the meta capability if the user has the required primitive capability
if ( user_can( $user, $required_cap ) ) {
$user_caps[ $args[0] ] = true;
}
}

return $user_caps;
}

/**
* Filters the array of row meta for each plugin in the Plugins list table.
*
Expand Down Expand Up @@ -153,7 +241,7 @@
$request = new Request();

if ( isset( $_POST['crontrol_action'] ) && ( 'new_cron' === $_POST['crontrol_action'] ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'create_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to add new cron events.', 'wp-crontrol' ), 401 );
}
check_admin_referer( 'crontrol-new-cron' );
Expand Down Expand Up @@ -211,7 +299,7 @@
exit;

} elseif ( isset( $_POST['crontrol_action'] ) && ( 'new_url_cron' === $_POST['crontrol_action'] ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'create_url_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to add new cron events.', 'wp-crontrol' ), 401 );
}
check_admin_referer( 'crontrol-new-cron' );
Expand Down Expand Up @@ -269,7 +357,7 @@
exit;

} elseif ( isset( $_POST['crontrol_action'] ) && ( 'new_php_cron' === $_POST['crontrol_action'] ) ) {
if ( ! current_user_can_manage_php_cron_events() ) {
if ( ! current_user_can( 'create_php_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to add new PHP cron events.', 'wp-crontrol' ), 401 );
}
check_admin_referer( 'crontrol-new-cron' );
Expand Down Expand Up @@ -327,15 +415,15 @@
exit;

} elseif ( isset( $_POST['crontrol_action'] ) && ( 'edit_cron' === $_POST['crontrol_action'] ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'edit_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to edit cron events.', 'wp-crontrol' ), 401 );
}

$cr = $request->init( wp_unslash( $_POST ) );

check_admin_referer( "crontrol-edit-cron_{$cr->original_hookname}_{$cr->original_sig}_{$cr->original_next_run_utc}" );

if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can_manage_php_cron_events() ) {
if ( 'crontrol_cron_job' === $cr->hookname && ! current_user_can( 'create_php_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
}

Expand Down Expand Up @@ -419,7 +507,7 @@
exit;

} elseif ( isset( $_POST['crontrol_action'] ) && ( 'edit_url_cron' === $_POST['crontrol_action'] ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'edit_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to edit cron events.', 'wp-crontrol' ), 401 );
}

Expand Down Expand Up @@ -510,7 +598,7 @@
exit;

} elseif ( isset( $_POST['crontrol_action'] ) && ( 'edit_php_cron' === $_POST['crontrol_action'] ) ) {
if ( ! current_user_can_manage_php_cron_events() ) {
if ( ! current_user_can( 'create_php_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to edit PHP cron events.', 'wp-crontrol' ), 401 );
}

Expand Down Expand Up @@ -633,7 +721,7 @@
exit;

} elseif ( ( isset( $_POST['action'] ) && 'crontrol_delete_crons' === $_POST['action'] ) || ( isset( $_POST['action2'] ) && 'crontrol_delete_crons' === $_POST['action2'] ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'delete_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
}
check_admin_referer( 'bulk-crontrol-events' );
Expand Down Expand Up @@ -676,7 +764,7 @@
exit;

} elseif ( isset( $_GET['crontrol_action'] ) && 'delete-cron' === $_GET['crontrol_action'] ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'delete_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
}
$hook = wp_unslash( $_GET['crontrol_id'] );
Expand Down Expand Up @@ -730,7 +818,7 @@
exit;

} elseif ( isset( $_GET['crontrol_action'] ) && 'delete-hook' === $_GET['crontrol_action'] ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'delete_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to delete cron events.', 'wp-crontrol' ), 401 );
}
$hook = wp_unslash( $_GET['crontrol_id'] );
Expand Down Expand Up @@ -778,7 +866,7 @@
exit;
}
} elseif ( isset( $_GET['crontrol_action'] ) && 'run-cron' === $_GET['crontrol_action'] ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'run_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to run cron events.', 'wp-crontrol' ), 401 );
}
$hook = wp_unslash( $_GET['crontrol_id'] );
Expand Down Expand Up @@ -818,7 +906,7 @@
wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
exit;
} elseif ( isset( $_GET['crontrol_action'] ) && 'pause-hook' === $_GET['crontrol_action'] ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'pause_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to pause or resume cron events.', 'wp-crontrol' ), 401 );
}

Expand Down Expand Up @@ -865,7 +953,7 @@
wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
exit;
} elseif ( isset( $_GET['crontrol_action'] ) && 'resume-hook' === $_GET['crontrol_action'] ) {
if ( ! current_user_can( 'manage_options' ) ) {
if ( ! current_user_can( 'pause_cron_event' ) ) {
wp_die( esc_html__( 'You are not allowed to pause or resume cron events.', 'wp-crontrol' ), 401 );
}

Expand Down Expand Up @@ -912,6 +1000,9 @@
wp_safe_redirect( add_query_arg( $redirect, admin_url( 'tools.php' ) ) );
exit;
} elseif ( isset( $_POST['crontrol_action'] ) && 'export-event-csv' === $_POST['crontrol_action'] ) {
if ( ! current_user_can( 'export_cron_events' ) ) {
wp_die( esc_html__( 'You are not allowed to export cron events.', 'wp-crontrol' ), 401 );
}
check_admin_referer( 'crontrol-export-event-csv', 'crontrol_nonce' );

$type = isset( $_POST['crontrol_hooks_type'] ) ? wp_unslash( $_POST['crontrol_hooks_type'] ) : 'all';
Expand Down Expand Up @@ -1020,7 +1111,7 @@
$tabs[] = add_management_page(
esc_html__( 'Cron Events', 'wp-crontrol' ),
esc_html__( 'Cron Events', 'wp-crontrol' ),
'manage_options',
'view_cron_events',
'wp-crontrol',
__NAMESPACE__ . '\admin_manage_page'
);
Expand Down Expand Up @@ -2853,5 +2944,5 @@
* Returns whether PHP cron events are enabled and can be managed by the current user.
*/
function current_user_can_manage_php_cron_events(): bool {
return ( php_cron_events_enabled() && current_user_can( 'edit_files' ) );
return ( php_cron_events_enabled() && current_user_can( 'create_php_cron_event' ) );
}
Loading