settings = $settings; } public function init() { add_action( 'admin_post_careback_track_save', array( $this, 'save' ) ); } /** * The settings this screen reads and writes. * * @return Settings */ public function settings() { return $this->settings; } /** * Is the settings screen being asked for the tracking desk? * * Read-only navigation state, so no nonce: it selects which half of one * screen renders and changes nothing. */ public static function requested() { return isset( $_GET['desk'] ) && 'track' === sanitize_key( wp_unslash( $_GET['desk'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only: which half of the screen renders. } /** * The two desks of the Settings tab, as links under the tabs: returns and * tracking. * * Settings is where a person goes to change something, so both desks are * reachable from there and nowhere else. A tab of its own read as a second, * nested application — which it is not. * * @param string $active The desk shown: Admin::SCREEN (`return`) or SCREEN (`track`). */ public static function switcher( $active ) { $base = admin_url( 'admin.php?page=careback-settings' ); $desks = array( 'return' => array( 'label' => __( 'Returns', 'careback' ), 'url' => $base, ), 'track' => array( 'label' => __( 'Tracking', 'careback' ), 'url' => add_query_arg( 'desk', 'track', $base ), ), ); $last = array_key_last( $desks ); ?>

settings ); for ( $i = 1; $i <= self::BLANK_CARRIERS; $i++ ) { $rows[] = array( 'slug' => 'new' . $i, 'name' => '', 'url' => '', 'enabled' => false, 'preset' => false ); } ?>
aria-label="" />

settings ); $stages = array_merge( Track_Stage_Policy::ORDER_LADDER, Track_Stage_Policy::ORDER_OFF_RAMPS ); foreach ( wc_get_order_statuses() as $slug => $name ) { $slug = preg_replace( '/^wc-/', '', $slug ); $current = isset( $map[ $slug ] ) ? $map[ $slug ] : ''; ?>  ● render(); } public function render() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } $s = $this->settings->all(); ?>

sections() as $section ) { call_user_func( $section['render'], $s, self::SCREEN, $this->settings ); } ?>
group, label, * position and the callable that prints it ( array $stored_settings, * string $screen, Settings $settings ), handed SCREEN and this screen's * settings: its own four, and those an add-on registers in the * group TAB through `careback_settings_sections`. Unlike the returns * screen, a section here prints its own heading, because each heading * carries a note. * * @return array */ public function sections() { $own = array( 'track-page' => array( 'tab' => self::TAB, 'label' => __( 'Page', 'careback' ), 'position' => 10, 'render' => array( $this, 'section_page' ), ), 'track-carriers' => array( 'tab' => self::TAB, 'label' => __( 'Delivery services', 'careback' ), 'position' => 20, 'render' => array( $this, 'section_carriers' ), ), 'track-limits' => array( 'tab' => self::TAB, 'label' => __( 'Abuse limits', 'careback' ), 'position' => 30, 'render' => array( $this, 'section_limits' ), ), 'track-statuses' => array( 'tab' => self::TAB, 'label' => __( 'Order statuses', 'careback' ), 'position' => 40, 'render' => array( $this, 'section_statuses' ), ), ); return array_filter( Admin::section_entries( $own ), static function ( $section ) { return self::TAB === $section['tab']; } ); } /** * Whether the page runs at all, and how much it says. Its rows are the * table `track-page` of `careback_settings_fields`. * * @param array $s Stored settings. */ public function section_page( array $s ) { self::heading( __( 'Page', 'careback' ), __( 'Whether the tracking page runs at all, and how much it says. Everything here is safe to change while the page is live.', 'careback' ) ); ?> array( 'position' => 10, 'render' => array( $this, 'field_enabled' ), ), 'track_show_items' => array( 'position' => 30, 'render' => array( $this, 'field_items' ), ), ), $s, self::SCREEN, $this->settings ); ?> settings; ?>

[careback_tracking]

settings; ?> settings; self::heading( __( 'Abuse limits', 'careback' ), __( 'This page is public: anybody may try order numbers against it. These three numbers are what makes guessing expensive. The defaults are sane — change them only if real customers are being refused.', 'careback' ) ); ?> status_rows(); ?> $row ) { if ( ! is_array( $row ) ) { continue; } $key = sanitize_key( (string) $key ); $preset = Track_Carriers::preset( $key ); $url = self::clean_template( isset( $row['url'] ) ? $row['url'] : '' ); $enabled = empty( $row['enabled'] ) ? 0 : 1; if ( $preset ) { $out[ $key ] = array( 'name' => $preset['name'], 'url' => $url === $preset['url'] ? '' : $url, 'enabled' => $enabled, ); continue; } $name = isset( $row['name'] ) ? sanitize_text_field( (string) $row['name'] ) : ''; // The name is the row. Emptying it is how a row is removed, which // beats a delete button that needs scripting to work. if ( '' === trim( $name ) ) { continue; } $slug = sanitize_key( $key ); if ( '' === $slug || 0 === strpos( $slug, 'new' ) ) { $slug = sanitize_key( sanitize_title( $name ) ); } if ( '' === $slug || Track_Carriers::preset( $slug ) ) { $slug = 'own-' . substr( md5( $name ), 0, 8 ); } $out[ $slug ] = array( 'name' => $name, 'url' => $url, 'enabled' => $enabled ); } return $out; } /** * Writes only this module's keys. * * Settings::update() merges over what is already stored, so everything the * returns screen owns survives untouched — which is the whole reason this * form is separate. */ public function save() { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'Not allowed.', 'careback' ) ); } check_admin_referer( self::NONCE ); // The form, read once after the capability and the nonce and sanitised as // it is read (Admin::posted_form()), for the sections an add-on registers // (`careback_save_settings`); each reader then validates what it stores. $posted = Admin::posted_form( self::NONCE ); $stages = array_merge( Track_Stage_Policy::ORDER_LADDER, Track_Stage_Policy::ORDER_OFF_RAMPS ); $map = array(); if ( isset( $posted['track_order_stage_map'] ) && is_array( $posted['track_order_stage_map'] ) ) { foreach ( $posted['track_order_stage_map'] as $status => $stage ) { $status = sanitize_key( $status ); $stage = is_string( $stage ) ? sanitize_key( $stage ) : ''; if ( '' === $status ) { continue; } // An empty selection is stored, not skipped. Skipping it let the // shipped default merge back in, so a shop could never take a // wrong mapping off a core status. if ( '' === $stage || in_array( $stage, $stages, true ) ) { $map[ $status ] = $stage; } } } // Statuses the shop has since removed leave orphan keys behind; drop them // rather than letting the array grow without limit. $known = array_map( static function ( $slug ) { return preg_replace( '/^wc-/', '', $slug ); }, array_keys( function_exists( 'wc_get_order_statuses' ) ? wc_get_order_statuses() : array() ) ); if ( ! empty( $known ) ) { $map = array_intersect_key( $map, array_flip( $known ) ); } $carriers = self::clean_carriers( isset( $posted['track_carriers'] ) ? $posted['track_carriers'] : array() ); // Every key this screen owns is written here; a section an add-on // registers adds its own through `careback_save_settings`, which hands it // this screen's id (SCREEN) and its settings. A key nobody sends is not // touched. $this->settings->update( (array) apply_filters( Admin::SAVE_FILTER, // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_save_settings, the final prefix. array( 'track_enabled' => isset( $_POST['track_enabled'] ) ? 1 : 0, 'track_max_attempts' => max( 1, isset( $_POST['track_max_attempts'] ) ? intval( wp_unslash( $_POST['track_max_attempts'] ) ) : 8 ), 'track_window_minutes' => max( 1, isset( $_POST['track_window_minutes'] ) ? intval( wp_unslash( $_POST['track_window_minutes'] ) ) : 15 ), 'track_floor_ms' => max( 150, isset( $_POST['track_floor_ms'] ) ? intval( wp_unslash( $_POST['track_floor_ms'] ) ) : 400 ), 'track_show_items' => isset( $_POST['track_show_items'] ) ? 1 : 0, 'track_carriers' => $carriers, 'track_order_stage_map' => $map, ), self::SCREEN, $this->settings, $posted ) ); wp_safe_redirect( add_query_arg( array( 'page' => 'careback-settings', 'desk' => 'track', 'updated' => '1', ), admin_url( 'admin.php' ) ) ); exit; } }