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 );
?>
render();
}
public function render() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
return;
}
$s = $this->settings->all();
?>
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' ) );
?>
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;
}
}