Settings -> Advanced -> REST API screen writes. The storage * format is not incidental: WooCommerce looks keys up by `wc_api_hash()` of the * consumer key and stores only the hash, so the plaintext key exists exactly * once, in the response we hand to AffClix. * * The key is read-only. AffClix never needs to write to the store, and a * read/write key sitting in a third party's database is a liability the * merchant did not ask for. */ class Affclix_Rest_Key { const OPTION_KEY_ID = 'affclix_rest_key_id'; /** * Create the key if we do not already have one. * * @return array{consumer_key:string,consumer_secret:string}|null Null when * a key already exists (the plaintext is unrecoverable by then) or * creation failed. */ public static function provision() { global $wpdb; if (self::exists()) { return null; } $user_id = self::owner_id(); if (! $user_id) { return null; } $consumer_key = 'ck_'.wc_rand_hash(); $consumer_secret = 'cs_'.wc_rand_hash(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- WooCommerce has no API for minting REST keys; this is the row its own settings screen writes. $inserted = $wpdb->insert( $wpdb->prefix.'woocommerce_api_keys', [ 'user_id' => $user_id, 'description' => 'AffClix (read-only)', 'permissions' => 'read', 'consumer_key' => wc_api_hash($consumer_key), 'consumer_secret' => $consumer_secret, 'truncated_key' => substr($consumer_key, -7), ], ['%d', '%s', '%s', '%s', '%s', '%s'] ); if (! $inserted) { return null; } update_option(self::OPTION_KEY_ID, (int) $wpdb->insert_id, false); return [ 'consumer_key' => $consumer_key, 'consumer_secret' => $consumer_secret, ]; } /** * Whether our key row is still present. * * Re-checked against the table rather than trusted from the option, because * a merchant can revoke the key from the REST API screen and we would * otherwise never re-provision. * * @return bool */ public static function exists() { global $wpdb; $key_id = (int) get_option(self::OPTION_KEY_ID, 0); if (! $key_id) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the table has no API, and a cached answer would hide a key the merchant just revoked. $found = $wpdb->get_var( $wpdb->prepare( "SELECT key_id FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ) ); return ! empty($found); } /** * Revoke the key we created. */ public static function revoke() { global $wpdb; $key_id = (int) get_option(self::OPTION_KEY_ID, 0); if ($key_id) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the table has no API. $wpdb->delete($wpdb->prefix.'woocommerce_api_keys', ['key_id' => $key_id], ['%d']); } delete_option(self::OPTION_KEY_ID); } /** * The user the key is issued to. * * REST permissions are evaluated against this user, so it has to be someone * who can actually read orders — the same constraint the webhook delivery * user has. * * @return int */ private static function owner_id() { if (current_user_can('manage_woocommerce')) { return get_current_user_id(); } $admins = get_users( [ 'role' => 'administrator', 'number' => 1, 'fields' => 'ID', 'orderby' => 'ID', ] ); return ! empty($admins) ? (int) $admins[0] : 0; } }