'', 'key_prefix' => '', 'signing_secret' => '', 'base_url' => 'https://affiliatant.com', 'pixel_url' => 'https://affiliatant.com/p.php', 'program_id' => 0, 'store_ref' => '', 'test_mode' => 0, 'enabled' => 1, 'paid_statuses' => array( 'processing', 'completed' ), 'cookie_days' => 30, 'beacon' => 1, 'pixel' => 1, 'timeout' => 5, 'report_all' => 1, 'last_event' => array(), 'last_error' => array(), ); } /** * All settings, defaults filled in. * * @return array */ public static function all() { if ( null === self::$cache ) { $stored = get_option( self::OPTION, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } self::$cache = array_merge( self::defaults(), $stored ); } return self::$cache; } /** * One setting. * * @param string $key Key. * @param mixed $default Fallback when the key is unknown. * @return mixed */ public static function get( $key, $default = '' ) { $all = self::all(); return array_key_exists( $key, $all ) ? $all[ $key ] : $default; } /** * Merge and save. * * @param array $values Keys to change. * @return void */ public static function save( array $values ) { $all = array_merge( self::all(), $values ); update_option( self::OPTION, $all, true ); self::$cache = $all; } /** Forget the request cache. Used by the tests on the admin screen path. */ public static function flush() { self::$cache = null; } /** * Enough configuration to talk to Affiliatant at all. * * @return bool */ public static function configured() { return '' !== (string) self::get( 'api_key' ) && (int) self::get( 'program_id' ) > 0; } /** * Installed, switched on and configured. * * @return bool */ public static function active() { return (int) self::get( 'enabled' ) === 1 && self::configured(); } /** * True when conversions should be flagged as test data. * * @return bool */ public static function test_mode() { return (int) self::get( 'test_mode' ) === 1 || self::key_is_test(); } /** * Does the KEY say test, whatever the switch says? * * The switch and the key are two different things and the key is the * one the API obeys: a conversion sent with aff_sk_test_... comes back * flagged as test data however this plugin is configured. Reading only * the switch would tell a merchant who had pasted a test key that the * mode was live while nothing they sent reached a live report. * * The switch still works the other way round: it can force test on a * live key, which is the useful direction. * * @return bool */ public static function key_is_test() { $prefix = (string) self::get( 'key_prefix' ); if ( '' === $prefix ) { $prefix = (string) self::get( 'api_key' ); } return 0 === strpos( $prefix, 'aff_sk_test_' ) || 0 === strpos( $prefix, 'aff_pk_test_' ); } /** * The statuses that mean "paid". * * Defaults to WooCommerce's own answer rather than a hard-coded pair: * wc_get_is_paid_statuses() returns processing and completed through the * woocommerce_order_is_paid_statuses filter, and a merchant with a custom * paid status has already told WooCommerce about it. * * @return array */ public static function paid_statuses() { $configured = self::get( 'paid_statuses' ); if ( ! is_array( $configured ) || array() === $configured ) { $configured = function_exists( 'wc_get_is_paid_statuses' ) ? wc_get_is_paid_statuses() : array( 'processing', 'completed' ); } $clean = array(); foreach ( $configured as $status ) { $status = sanitize_key( (string) $status ); // WooCommerce stores the internal status with a wc- prefix and // hands the hooks the bare one. Everything here is bare. if ( 0 === strpos( $status, 'wc-' ) ) { $status = substr( $status, 3 ); } if ( '' !== $status ) { $clean[] = $status; } } return array_values( array_unique( $clean ) ); } /** * A configured client. * * @return AffiliatantClient */ public static function client() { $timeout = (int) self::get( 'timeout' ); if ( $timeout < 1 || $timeout > 30 ) { $timeout = 5; } $client = new AffiliatantClient( (string) self::get( 'api_key' ), array( 'base_url' => (string) self::get( 'base_url' ), 'signing_secret' => (string) self::get( 'signing_secret' ), // Short on purpose. This can run inside a checkout request on // an install with no Action Scheduler, and a tracker that can // hold a shopper on a blank page is uninstalled the same day. 'timeout' => $timeout, 'connect_timeout' => 2, 'user_agent' => 'affiliatant-for-woocommerce/' . AFFILIATANT_WOO_VERSION . ' wc/' . ( defined( 'WC_VERSION' ) ? WC_VERSION : '?' ) . ' wp/' . get_bloginfo( 'version' ), ) ); // Inside WordPress, WordPress does the HTTP. The SDK's own cURL path // never runs here: it ignores WP_PROXY_HOST, walks past // pre_http_request and block_local_requests, and fails outright on a // host with cURL disabled. See class-affiliatant-http.php. return $client->setTransport( array( 'Affiliatant_Http', 'request' ) ); } /** * Record what just went out, for the status panel. * * @param string $action conversion or reverse. * @param string $event_id Event id. * @param int $order_id Order id. * @return void */ public static function note_event( $action, $event_id, $order_id ) { self::save( array( 'last_event' => array( 'action' => (string) $action, 'event_id' => (string) $event_id, 'order_id' => (int) $order_id, 'at' => time(), ), ) ); } /** * Record the last thing that went wrong. * * The message comes from Affiliatant's own error body. It never contains * the API key: the SDK keeps the key out of its exception messages and * out of every URL, for exactly this reason. * * @param string $message Error text. * @param int $order_id Order id, 0 when not about an order. * @return void */ public static function note_error( $message, $order_id = 0 ) { self::save( array( 'last_error' => array( 'message' => substr( (string) $message, 0, 500 ), 'order_id' => (int) $order_id, 'at' => time(), ), ) ); } }