default. * * @return array */ public static function defaults() { return array( 'recast_export_retention_hours' => 48, 'recast_export_sync_threshold' => 2000, 'recast_apply_chunk_size' => 25, 'recast_apply_lanes' => 1, 'recast_journal_retention_days' => 180, 'recast_csv_bom' => true, 'recast_purge_on_uninstall' => false, 'recast_quiet_window_minutes' => 15, 'recast_max_cohort_size' => 25000, 'recast_preview_ttl_hours' => 24, ); } /** * Reads an option with its default. * * @param string $name Option name (with recast_ prefix). * @return mixed */ public static function get( $name ) { $defaults = self::defaults(); $default = isset( $defaults[ $name ] ) ? $defaults[ $name ] : null; $value = get_option( $name, $default ); if ( is_bool( $default ) ) { if ( is_string( $value ) ) { return in_array( $value, array( 'yes', '1', 'true' ), true ); } return (bool) $value; } if ( is_int( $default ) ) { return (int) $value; } return $value; } /** * Validation rules: option => [type, min, max]. * * @return array */ private static function rules() { return array( 'recast_export_retention_hours' => array( 'int', 1, 720 ), 'recast_export_sync_threshold' => array( 'int', 1, 100000 ), 'recast_apply_chunk_size' => array( 'int', 1, 200 ), 'recast_apply_lanes' => array( 'int', 1, 4 ), 'recast_journal_retention_days' => array( 'int', 0, 3650 ), 'recast_csv_bom' => array( 'bool' ), 'recast_purge_on_uninstall' => array( 'bool' ), 'recast_quiet_window_minutes' => array( 'int', 0, 1440 ), 'recast_max_cohort_size' => array( 'int', 100, 1000000 ), 'recast_preview_ttl_hours' => array( 'int', 1, 168 ), ); } /** * Settings shown in the admin app: key (without prefix) => label, help, type. * * @return array */ public static function fields() { return array( 'export_sync_threshold' => array( 'label' => __( 'Download at once up to this many subscriptions', 'recast-subscriptions' ), 'help' => __( 'Recast exports larger selections in the background.', 'recast-subscriptions' ), 'type' => 'int', ), 'export_retention_hours' => array( 'label' => __( 'Keep export files for this many hours', 'recast-subscriptions' ), 'type' => 'int', ), 'csv_bom' => array( 'label' => __( 'Add a UTF-8 byte order mark to CSV files', 'recast-subscriptions' ), 'help' => __( 'Excel needs it to show accented characters correctly.', 'recast-subscriptions' ), 'type' => 'bool', ), 'apply_chunk_size' => array( 'label' => __( 'Subscriptions per background step (1 to 200)', 'recast-subscriptions' ), 'type' => 'int', ), 'apply_lanes' => array( 'label' => __( 'Background workers (1 to 4)', 'recast-subscriptions' ), 'help' => __( 'Workers apply a batch at the same time. Use more than 1 only on a server with spare capacity.', 'recast-subscriptions' ), 'type' => 'int', ), 'quiet_window_minutes' => array( 'label' => __( 'Skip a subscription if its renewal is due in less than this many minutes', 'recast-subscriptions' ), 'type' => 'int', ), 'preview_ttl_hours' => array( 'label' => __( 'A preview expires after this many hours', 'recast-subscriptions' ), 'type' => 'int', ), 'max_cohort_size' => array( 'label' => __( 'Maximum subscriptions per batch', 'recast-subscriptions' ), 'type' => 'int', ), 'journal_retention_days' => array( 'label' => __( 'Delete batch details after this many days', 'recast-subscriptions' ), 'help' => __( '0 keeps them. After deletion you cannot undo the batch.', 'recast-subscriptions' ), 'type' => 'int', ), 'purge_on_uninstall' => array( 'label' => __( 'Delete all Recast data when you uninstall the plugin', 'recast-subscriptions' ), 'type' => 'bool', ), ); } /** * Every setting keyed without the prefix. * * @return array */ public static function all() { $out = array(); foreach ( array_keys( self::defaults() ) as $name ) { $out[ substr( $name, strlen( 'recast_' ) ) ] = self::get( $name ); } return $out; } /** * Validates and stores settings (keys without the prefix). * * @param array $values Values. * @return true|\WP_Error */ public static function update( array $values ) { $rules = self::rules(); $fields = self::fields(); $errors = array(); $clean = array(); foreach ( $values as $key => $value ) { $name = 'recast_' . sanitize_key( (string) $key ); if ( ! isset( $rules[ $name ] ) ) { continue; } $rule = $rules[ $name ]; if ( 'bool' === $rule[0] ) { $clean[ $name ] = in_array( $value, array( true, 1, '1', 'true', 'yes', 'on' ), true ); continue; } if ( ! is_numeric( $value ) || (int) $value < $rule[1] || (int) $value > $rule[2] ) { $errors[] = array( 'key' => (string) $key, 'label' => isset( $fields[ $key ]['label'] ) ? $fields[ $key ]['label'] : (string) $key, /* translators: 1: minimum, 2: maximum */ 'message' => sprintf( __( 'Enter a number from %1$d to %2$d.', 'recast-subscriptions' ), $rule[1], $rule[2] ), 'min' => $rule[1], 'max' => $rule[2], ); continue; } $clean[ $name ] = (int) $value; } if ( $errors ) { $lines = array_map( static function ( $error ) { /* translators: 1: setting, 2: min, 3: max */ return sprintf( __( '%1$s must be between %2$d and %3$d.', 'recast-subscriptions' ), $error['label'], $error['min'], $error['max'] ); }, $errors ); return new \WP_Error( 'recast_invalid_settings', implode( ' ', $lines ), array( 'status' => 400, 'errors' => array_map( static function ( $error ) { return array( 'key' => $error['key'], 'message' => $error['message'], ); }, $errors ), ) ); } foreach ( $clean as $name => $value ) { // Booleans are stored as yes/no: update_option() skips writes when a new option's value is false. update_option( $name, is_bool( $value ) ? ( $value ? 'yes' : 'no' ) : $value, false ); } return true; } const GUIDE_META = 'recast_guide_dismissed'; /** * Whether the current user closed the "How it works" guide. * * @return bool */ public static function guide_dismissed() { return (bool) get_user_meta( get_current_user_id(), self::GUIDE_META, true ); } /** * Stores the guide preference for the current user. * * @param bool $dismissed Dismissed. * @return void */ public static function set_guide_dismissed( $dismissed ) { if ( $dismissed ) { update_user_meta( get_current_user_id(), self::GUIDE_META, '1' ); } else { delete_user_meta( get_current_user_id(), self::GUIDE_META ); } } /** * Data exposed as window.recastSettings. * * @return array */ public static function for_js() { $wcs_version = class_exists( 'WC_Subscriptions' ) ? (string) \WC_Subscriptions::$version : ''; return array( 'restNamespace' => 'recast/v1', 'version' => RECAST_VERSION, 'wcVersion' => defined( 'WC_VERSION' ) ? WC_VERSION : '', 'wcsVersion' => $wcs_version, 'hpos' => Hpos::is_enabled(), 'proUrl' => 'https://recastsubscriptions.com/#pricing', 'caps' => array( 'read' => current_user_can( Auth::read_capability() ), ), 'storeCurrency' => get_woocommerce_currency(), 'currencySymbol' => html_entity_decode( get_woocommerce_currency_symbol() ), 'priceDecimals' => wc_get_price_decimals(), 'pricesIncludeTax' => wc_prices_include_tax(), 'timezone' => wp_timezone_string(), 'dateFormat' => get_option( 'date_format' ), 'exportSyncThreshold' => self::get( 'recast_export_sync_threshold' ), 'previewPerPage' => 25, 'subscriptionEditUrl' => admin_url( 'admin.php?page=wc-orders--shop_subscription&action=edit&id=%d' ), 'adminUrl' => admin_url(), 'operations' => \Recast\WCS\Plugin::instance()->operations()->descriptors(), 'labels' => Labels::for_js(), 'settingsFields' => self::fields(), 'guideDismissed' => self::guide_dismissed(), 'shell' => Shell::for_js(), ); } }