'; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * Screens: key => [ slug suffix, title ]. * * @return array */ public static function screens() { $screens = array( 'dashboard' => array( '', __( 'Dashboard', 'returnkit' ) ), 'requests' => array( '-requests', __( 'Requests', 'returnkit' ) ), 'settings' => array( '-settings', __( 'Settings', 'returnkit' ) ), 'help' => array( '-help', __( 'Help', 'returnkit' ) ), ); /** * Filters the ReturnKit admin screens (Pro adds Insights). * * @param array $screens key => [ slug suffix, title ]. */ $screens = (array) apply_filters( 'rtkt_admin_screens', $screens ); if ( ! Gate::is_pro() ) { $screens['pro'] = array( '-pro', __( 'Go Pro', 'returnkit' ) ); } return $screens; } /** * Register menu pages. * * @return void */ public static function menu() { $pending = Requests::count( array( 'status' => 'pending' ) ); $bubble = $pending ? ' ' . esc_html( number_format_i18n( $pending ) ) . '' : ''; add_menu_page( __( 'ReturnKit', 'returnkit' ), __( 'Returns', 'returnkit' ) . $bubble, self::CAP, RTKT_MENU_SLUG, array( __CLASS__, 'screen_dashboard' ), self::menu_icon(), '56.6' ); foreach ( self::screens() as $key => $screen ) { $label = $screen[1]; if ( 'pro' === $key ) { $label = '' . esc_html( $screen[1] ) . ''; } elseif ( 'requests' === $key ) { $label .= $bubble; } $callback = method_exists( __CLASS__, 'render_' . $key ) ? array( __CLASS__, 'screen_' . $key ) : static function () use ( $key ) { /** * Render a screen added through rtkt_admin_screens. */ do_action( 'rtkt_admin_screen_' . $key ); }; add_submenu_page( RTKT_MENU_SLUG, $screen[1], $label, self::CAP, RTKT_MENU_SLUG . $screen[0], $callback ); } } /** * URL of a screen. * * @param string $screen Screen key. * @param array $args Extra query args. * @return string */ public static function url( $screen = 'dashboard', array $args = array() ) { $screens = self::screens(); $suffix = isset( $screens[ $screen ] ) ? $screens[ $screen ][0] : '-' . sanitize_key( $screen ); $args = array_filter( $args, static function ( $v ) { return '' !== (string) $v; } ); return add_query_arg( array_merge( array( 'page' => RTKT_MENU_SLUG . $suffix ), $args ), admin_url( 'admin.php' ) ); } /** * URL of a request in the admin. * * @param int $id Request ID. * @return string */ public static function request_url( $id ) { return self::url( 'requests', array( 'request' => (int) $id ) ); } /** * Is the current screen one of ours? * * @return bool */ public static function is_our_screen() { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return 0 === strpos( $page, RTKT_MENU_SLUG ); } /** * Body class. * * @param string $classes Classes. * @return string */ public static function body_class( $classes ) { return self::is_our_screen() ? $classes . ' rtkt-screen' : $classes; } /** * Enqueue assets on our screens. * * @return void */ public static function assets() { wp_add_inline_style( 'common', '#adminmenu .rtkt-menu-pro{color:#fbbf24;font-weight:600}' ); if ( ! self::is_our_screen() ) { return; } wp_enqueue_style( 'rtkt-admin', RTKT_URL . 'assets/css/admin.css', array(), rtkt_asset_version( 'assets/css/admin.css' ) ); wp_enqueue_script( 'rtkt-admin', RTKT_URL . 'assets/js/admin.js', array(), rtkt_asset_version( 'assets/js/admin.js' ), true ); wp_localize_script( 'rtkt-admin', 'ReturnKit', array( 'defaults' => Settings::defaults(), 'keep' => Settings::personal_keys(), 'i18n' => array( 'unsaved' => __( 'You have unsaved changes', 'returnkit' ), 'resetDone' => __( 'Recommended values filled in. Save to apply them.', 'returnkit' ), 'confirmReject' => __( 'Decline this request? The customer is emailed your reason.', 'returnkit' ), 'confirmRefund' => __( 'Refund this amount now? This can\'t be undone.', 'returnkit' ), 'confirmDelete' => __( 'Delete this request and its history permanently?', 'returnkit' ), ), ) ); /** * Fires after the ReturnKit admin assets are enqueued (Pro adds its own). */ do_action( 'rtkt_admin_assets' ); } /** * Redirect to the dashboard after a single-plugin activation. * * @return void */ public static function activation_redirect() { if ( ! get_transient( Installer::REDIRECT_KEY ) ) { return; } delete_transient( Installer::REDIRECT_KEY ); if ( wp_doing_ajax() || is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( self::CAP ) || ! ( rtkt_fs() instanceof Stub ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } wp_safe_redirect( self::url( 'dashboard' ) ); exit; } /** * Plugins-list links. * * @param array $links Links. * @return array */ public static function action_links( $links ) { $ours = array( '' . esc_html__( 'Requests', 'returnkit' ) . '', '' . esc_html__( 'Settings', 'returnkit' ) . '', ); if ( ! Gate::is_pro() ) { $ours[] = '' . esc_html__( 'Go Pro', 'returnkit' ) . ''; } return array_merge( $ours, (array) $links ); } /** * Docs and support links under the plugin description. * * @param array $meta Row meta. * @param string $file Plugin file. * @return array */ public static function row_meta( $meta, $file ) { if ( RTKT_BASENAME === $file ) { $meta[] = '' . esc_html__( 'Documentation', 'returnkit' ) . ''; $meta[] = '' . esc_html__( 'Support', 'returnkit' ) . ''; } return $meta; } /* ------------------------------------------------------------------ * Data * ------------------------------------------------------------------ */ /** * Withdrawal compliance checklist: [ label, ok (true|false|null = check by hand), hint, link ]. * * @return array[] */ public static function compliance() { $s = Settings::all(); $page = Access::withdrawal_base(); $emails = get_option( 'woocommerce_rtkt_customer_withdrawal_settings', array() ); $ack_on = ! is_array( $emails ) || ! isset( $emails['enabled'] ) || 'no' !== $emails['enabled']; $in_menu = $page && self::page_in_menu( (int) $s['withdrawal_page'] ); $settings = self::url( 'settings' ); return array( array( __( 'Withdrawal function switched on', 'returnkit' ), (bool) $s['withdrawal_enabled'], '', $settings . '#rtkt-withdrawal' ), array( __( 'Withdrawal page published', 'returnkit' ), '' !== $page, __( 'Create the pages from Settings → Pages.', 'returnkit' ), $settings . '#rtkt-pages' ), array( __( '"Withdraw from contract here" link on every page', 'returnkit' ), $s['withdrawal_in_footer'] || $in_menu ? true : null, __( 'Turn on the footer link, add the page to your footer menu, or place [returnkit_withdrawal_button] in your footer. Then check it shows.', 'returnkit' ), $settings . '#rtkt-withdrawal', ), array( __( 'Acknowledgement email enabled', 'returnkit' ), $ack_on, __( 'Turn on "Withdrawal acknowledgement" in WooCommerce → Settings → Emails.', 'returnkit' ), admin_url( 'admin.php?page=wc-settings&tab=email§ion=rtkt_customer_withdrawal' ) ), array( __( 'Withdrawal links in My Account and order emails', 'returnkit' ), $s['withdrawal_in_account'] && $s['withdrawal_in_emails'], '', $settings . '#rtkt-withdrawal' ), array( __( 'Only one withdrawal form active', 'returnkit' ), ! CoreWithdrawal::enabled(), __( 'WooCommerce\'s own withdrawal form is also on.', 'returnkit' ), CoreWithdrawal::settings_url() ), ); } /** * Is a page linked in any navigation menu? * * @param int $page_id Page ID. * @return bool */ private static function page_in_menu( $page_id ) { if ( ! $page_id ) { return false; } foreach ( wp_get_nav_menus() as $menu ) { foreach ( (array) wp_get_nav_menu_items( $menu->term_id ) as $item ) { if ( 'page' === $item->object && (int) $item->object_id === $page_id ) { return true; } } } // Block themes: navigation blocks linking the page. $nav = get_posts( array( 'post_type' => 'wp_navigation', 'posts_per_page' => 20, 'post_status' => 'publish', ) ); foreach ( $nav as $post ) { if ( false !== strpos( (string) $post->post_content, '"id":' . $page_id ) || false !== strpos( (string) $post->post_content, 'returnkit_withdrawal_button' ) ) { return true; } } return false; } /** * A flash message from a redirect, as [ text, is_error ]. * * @return array|null */ public static function flash() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $msg = isset( $_GET['rtkt_msg'] ) ? sanitize_key( wp_unslash( $_GET['rtkt_msg'] ) ) : ''; // phpcs:enable if ( 'error' === $msg ) { $key = 'rtkt_flash_' . get_current_user_id(); $error = (string) get_transient( $key ); delete_transient( $key ); return '' !== $error ? array( $error, true ) : null; } $map = array( 'saved' => array( __( 'Settings saved', 'returnkit' ), false ), 'pages' => array( __( 'Pages created', 'returnkit' ), false ), 'approved' => array( __( 'Request approved and the customer emailed', 'returnkit' ), false ), 'rejected' => array( __( 'Request declined and the customer emailed', 'returnkit' ), false ), 'received' => array( __( 'Marked as received', 'returnkit' ), false ), 'refunded' => array( __( 'Refund issued and the request completed', 'returnkit' ), false ), 'completed' => array( __( 'Request completed', 'returnkit' ), false ), 'noted' => array( __( 'Note added', 'returnkit' ), false ), 'messaged' => array( __( 'Message sent to the customer', 'returnkit' ), false ), 'deleted' => array( __( 'Request deleted', 'returnkit' ), false ), 'linked' => array( __( 'Request linked to the order', 'returnkit' ), false ), ); /** * Filters the admin flash messages (Pro adds its own). * * @param array $map key => [ text, is_error ]. */ $map = (array) apply_filters( 'rtkt_admin_flash', $map ); return isset( $map[ $msg ] ) ? $map[ $msg ] : null; } /* ------------------------------------------------------------------ * Screens * ------------------------------------------------------------------ */ /** * Admin page callbacks (screen_dashboard …): render guarded, so an error * shows a notice instead of a "critical error" page. * * @param string $name Method name. * @param array $args Arguments. * @return void */ public static function __callStatic( $name, $args ) { unset( $args ); if ( 0 !== strpos( $name, 'screen_' ) ) { return; } $method = 'render_' . substr( $name, 7 ); if ( ! method_exists( __CLASS__, $method ) ) { return; } $level = ob_get_level(); try { call_user_func( array( __CLASS__, $method ) ); } catch ( \Throwable $e ) { while ( ob_get_level() > $level ) { ob_end_clean(); } Safe::report( $e, 'admin screen ' . substr( $name, 7 ) ); echo '

' . esc_html__( 'This ReturnKit screen could not be shown. Your store is not affected. Details:', 'returnkit' ) . ' ' . esc_html( $e->getMessage() ) . '

'; } } /** * Render a view. * * @param string $view View file (without .php), relative to views/. * @param string $active Active tab key. * @param array $vars Variables for the view. * @return void */ public static function view( $view, $active, array $vars = array() ) { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'returnkit' ) ); } $flash = self::flash(); extract( $vars, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract include RTKT_DIR . 'views/' . $view . '.php'; } /** * Dashboard. * * @return void */ public static function render_dashboard() { $since = gmdate( 'Y-m-d H:i:s', time() - 30 * DAY_IN_SECONDS ); $counts = Requests::status_counts(); $recent = Requests::query( array(), 8 ); $month = Requests::query( array( 'since' => $since ), 1000 ); $value = 0.0; $refund = 0.0; $kept = 0.0; foreach ( $month as $request ) { if ( in_array( $request->status(), array( 'cancelled', 'rejected' ), true ) ) { continue; } $value += $request->total(); $refund += $request->refunded(); if ( in_array( $request->resolution(), array( 'credit', 'exchange' ), true ) ) { $kept += $request->total(); } } $core = CoreWithdrawal::flagged_orders( 50 ); // Next up: the oldest request waiting for review, else the oldest // with items back. Queries are newest first, so take the last one. $next = null; foreach ( array( 'pending', 'received' ) as $status ) { $waiting = (int) ( $counts[ $status ] ?? 0 ); if ( $waiting ) { $rows = Requests::query( array( 'status' => $status ), 1, $waiting - 1 ); $next = $rows ? $rows[0] : null; break; } } self::view( 'dashboard', 'dashboard', array( 'counts' => $counts, 'recent' => $recent, 'month' => count( $month ), 'value' => $value, 'refunded' => $refund, 'kept' => $kept, 'compliance' => self::compliance(), 'core_flags' => $core, 'next' => $next, 'is_pro' => Gate::is_pro(), 'portal' => '' !== Access::portal_base() && (int) Settings::get( 'portal_page' ) && 'publish' === get_post_status( (int) Settings::get( 'portal_page' ) ), 'address' => '' !== trim( (string) Settings::get( 'return_address' ) ), ) ); } /** * Requests list, or one request. * * @return void */ public static function render_requests() { // phpcs:disable WordPress.Security.NonceVerification.Recommended $id = isset( $_GET['request'] ) ? absint( $_GET['request'] ) : 0; if ( $id ) { self::render_request( $id ); return; } $statuses = Labels::statuses(); $status = isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : ''; $status = isset( $statuses[ $status ] ) ? $status : ''; $types = Labels::types(); $type = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : ''; $type = isset( $types[ $type ] ) ? $type : ''; $search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; $paged = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; $flagged = ! empty( $_GET['flagged'] ); // phpcs:enable $args = array( 'status' => $status, 'type' => $type, 'search' => $search, 'flagged' => $flagged, ); $total = Requests::count( $args ); self::view( 'requests', 'requests', array( 'statuses' => $statuses, 'status' => $status, 'types' => $types, 'type' => $type, 'search' => $search, 'flagged' => $flagged, 'counts' => Requests::status_counts(), 'paged' => $paged, 'total' => $total, 'pages' => (int) ceil( $total / self::PER_PAGE ), 'rows' => Requests::query( $args, self::PER_PAGE, ( $paged - 1 ) * self::PER_PAGE ), ) ); } /** * One request. * * @param int $id Request ID. * @return void */ private static function render_request( $id ) { $request = Requests::get( $id ); if ( ! $request ) { self::view( 'missing', 'requests' ); return; } $order = $request->order(); $plan = $order ? Workflow::refund_plan( $request, $order, false ) : array( 'total' => 0 ); $shipping = $order ? Workflow::refund_plan( $request, $order, true ) : array( 'total' => 0 ); self::view( 'request', 'requests', array( 'request' => $request, 'order' => $order, 'log' => Log::for_request( $request->id() ), 'refund_items' => $order ? min( $plan['total'], (float) $order->get_remaining_refund_amount() ) : 0, 'refund_ship' => $order ? min( $shipping['total'], (float) $order->get_remaining_refund_amount() ) : 0, 'gateway_refund' => $order ? Workflow::gateway_can_refund( $order ) : false, 'can' => array( 'approve' => Workflow::can( $request, 'approved' ), 'receive' => Workflow::can( $request, 'received' ), 'complete' => Workflow::can( $request, 'completed' ), 'reject' => Workflow::can( $request, 'rejected' ), ), 'is_pro' => Gate::is_pro(), ) ); } /** * Settings. * * @return void */ public static function render_settings() { $stored = get_option( Settings::OPTION, array() ); self::view( 'settings', 'settings', array( 'settings' => Settings::all(), 'stored' => wp_parse_args( is_array( $stored ) ? $stored : array(), Settings::defaults() ), 'is_pro' => Gate::is_pro(), 'statuses' => wc_get_order_statuses(), 'categories' => get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'number' => 500, ) ), ) ); } /** * Help. * * @return void */ public static function render_help() { self::view( 'help', 'help', array( 'is_pro' => Gate::is_pro() ) ); } /** * Go Pro. * * @return void */ public static function render_pro() { self::view( 'pro', 'pro', array( 'has_sdk' => ! ( rtkt_fs() instanceof Stub ) ) ); } }