%d', $pending );
}
add_menu_page(
__( 'Tillkeeper', 'tillkeeper' ),
$label,
'manage_woocommerce',
'tillkeeper',
'tlkp_render_admin_page',
'dashicons-shield',
56
);
// Same page, but the first submenu item gets a plain label instead of the count.
add_submenu_page( 'tillkeeper', __( 'Tillkeeper', 'tillkeeper' ), __( 'Overview', 'tillkeeper' ), 'manage_woocommerce', 'tillkeeper', 'tlkp_render_admin_page' );
}
add_action( 'admin_menu', 'tlkp_register_admin_page' );
/**
* @param string $hook Admin page hook.
*/
function tlkp_admin_assets( $hook ) {
if ( 'toplevel_page_tillkeeper' !== $hook ) {
return;
}
wp_enqueue_style( 'tlkp-admin', TLKP_PLUGIN_URL . 'assets/css/admin.css', array(), TLKP_VERSION );
wp_enqueue_script( 'tlkp-admin', TLKP_PLUGIN_URL . 'assets/js/admin.js', array(), TLKP_VERSION, array( 'in_footer' => true ) );
}
add_action( 'admin_enqueue_scripts', 'tlkp_admin_assets' );
/**
* Saves settings and clears the log (both nonce-checked form posts).
*/
function tlkp_handle_admin_forms() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to do this.', 'tillkeeper' ) );
}
check_admin_referer( 'tlkp_admin_form' );
$form = isset( $_POST['tlkp_form'] ) ? sanitize_key( wp_unslash( $_POST['tlkp_form'] ) ) : '';
if ( 'settings' === $form ) {
tlkp_update_settings(
array(
'guard_product_delete' => ! empty( $_POST['guard_product_delete'] ),
'order_status_mode' => isset( $_POST['order_status_mode'] ) ? sanitize_key( wp_unslash( $_POST['order_status_mode'] ) ) : '',
)
);
$notice = 'settings_saved';
} else {
tlkp_activity_log_clear();
$notice = 'log_cleared';
}
wp_safe_redirect( add_query_arg( 'tlkp_notice', $notice, admin_url( 'admin.php?page=tillkeeper' ) ) );
exit;
}
add_action( 'admin_post_tlkp_admin_form', 'tlkp_handle_admin_forms' );
/**
* Notice after a redirect. The actions that set it checked their own nonce.
*/
function tlkp_render_notice() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only flag set after a nonce-checked redirect.
$key = isset( $_GET['tlkp_notice'] ) ? sanitize_key( wp_unslash( $_GET['tlkp_notice'] ) ) : '';
$messages = array(
'approve_ok' => array( 'success', __( 'Approved and applied.', 'tillkeeper' ) ),
'approve_failed' => array( 'error', __( 'Could not run that request. See the activity log for the error.', 'tillkeeper' ) ),
'reject_ok' => array( 'success', __( 'Rejected. Nothing was changed.', 'tillkeeper' ) ),
'reject_failed' => array( 'error', __( 'Could not reject that request.', 'tillkeeper' ) ),
'settings_saved' => array( 'success', __( 'Settings saved.', 'tillkeeper' ) ),
'log_cleared' => array( 'success', __( 'Activity log cleared.', 'tillkeeper' ) ),
);
if ( isset( $messages[ $key ] ) ) {
printf( '
', esc_attr( $messages[ $key ][0] ), esc_html( $messages[ $key ][1] ) );
}
}
/**
* @param int $user_id User ID.
* @return string Login or a placeholder.
*/
function tlkp_user_label( $user_id ) {
$user = $user_id ? get_userdata( (int) $user_id ) : false;
return $user ? $user->user_login : __( '(unknown)', 'tillkeeper' );
}
/**
* @param string $type Object type.
* @param int $id Object ID.
* @return string Plain text.
*/
function tlkp_object_label( $type, $id ) {
if ( ! $id ) {
return '' === $type ? '—' : $type;
}
if ( 'product' === $type && function_exists( 'wc_get_product' ) ) {
$product = wc_get_product( $id );
if ( $product ) {
return sprintf( '%s #%d', $product->get_name(), $id );
}
}
/* translators: 1: object type, 2: object ID. */
return sprintf( __( '%1$s #%2$d', 'tillkeeper' ), $type, $id );
}
/**
* Renders the page.
*/
function tlkp_render_admin_page() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter.
$kind = isset( $_GET['kind'] ) && 'write' === $_GET['kind'] ? 'write' : '';
$pending = tlkp_approval_queue_get_pending();
$resolved = tlkp_approval_queue_get_resolved( 10 );
$settings = tlkp_get_settings();
$log = tlkp_activity_log_get( 100, $kind );
?>
__( 'Done', 'tillkeeper' ),
'applied' => __( 'Applied', 'tillkeeper' ),
'pending_approval' => __( 'Held for approval', 'tillkeeper' ),
'preview' => __( 'Preview only', 'tillkeeper' ),
'approved' => __( 'Approved and applied', 'tillkeeper' ),
'rejected' => __( 'Rejected', 'tillkeeper' ),
'error' => __( 'Failed', 'tillkeeper' ),
);
return isset( $labels[ $outcome ] ) ? $labels[ $outcome ] : $outcome;
}
/**
* A short, PII-free summary of what a queued call would do.
*
* @param array $entry Queue entry.
* @return string Leading space + summary, or empty.
*/
function tlkp_describe_input( array $entry ) {
$input = isset( $entry['input'] ) && is_array( $entry['input'] ) ? $entry['input'] : array();
if ( 'woocommerce/order-update-status' === $entry['ability'] && isset( $input['status'] ) ) {
/* translators: %s: order status. */
return ' ' . sprintf( __( '→ %s', 'tillkeeper' ), sanitize_key( $input['status'] ) );
}
if ( 'woocommerce/product-delete' === $entry['ability'] ) {
return ! empty( $input['force'] ) ? ' ' . __( '(permanently)', 'tillkeeper' ) : ' ' . __( '(to trash)', 'tillkeeper' );
}
return '';
}
/**
* @return array[] One row per WooCommerce / Tillkeeper ability.
*/
function tlkp_store_abilities_overview() {
if ( ! function_exists( 'wp_get_abilities' ) ) {
return array();
}
$guarded = tlkp_guarded_abilities();
$rows = array();
foreach ( wp_get_abilities() as $ability ) {
$name = $ability->get_name();
if ( ! tlkp_is_logged_ability( $name ) ) {
continue;
}
$annotations = (array) $ability->get_meta_item( 'annotations', array() );
$read = ! empty( $annotations['readonly'] );
$destructive = ! empty( $annotations['destructive'] );
if ( $read ) {
$type = __( 'Read', 'tillkeeper' );
} elseif ( $destructive ) {
$type = __( 'Change (destructive)', 'tillkeeper' );
} else {
$type = __( 'Change', 'tillkeeper' );
}
if ( isset( $guarded[ $name ] ) ) {
$level = 'guarded';
$protection = __( 'Guarded', 'tillkeeper' );
} elseif ( $read ) {
$level = 'logged';
$protection = __( 'Logged', 'tillkeeper' );
} else {
$level = $destructive ? 'open' : 'logged';
$protection = $destructive ? __( 'Logged only — runs immediately', 'tillkeeper' ) : __( 'Logged', 'tillkeeper' );
}
$rows[] = array(
'name' => $name,
'label' => $ability->get_label(),
'type' => $type,
'level' => $level,
'protection' => $protection,
);
}
usort(
$rows,
static function ( $a, $b ) {
return strcmp( $a['name'], $b['name'] );
}
);
return $rows;
}