$page ), $args ), admin_url( 'admin.php' ) ); } /** * The menu icon: Monthwrap's monogram, filled, so WordPress can recolour it to match * the admin colour scheme. * * @return string */ private static function icon() { $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- the menu icon, as WordPress expects it. } /** * A "Settings" link next to Deactivate on the Plugins screen. * * @param array $links Existing links. * @return array */ public static function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'monthwrap' ) . '' ); return $links; } /** * Whether the current admin screen is one of Monthwrap's. * * @param string $hook The admin page's hook suffix. * @return bool */ private static function ours( $hook ) { return false !== strpos( (string) $hook, self::SLUG ); } /** * Styles and scripts on Monthwrap screens only; the media library on Settings. * * @param string $hook The admin page's hook suffix. */ public static function assets( $hook ) { if ( ! self::ours( $hook ) ) { return; } wp_enqueue_style( 'monthwrap-admin', plugins_url( 'assets/admin.css', MONTHWRAP_FILE ), array(), MONTHWRAP_VERSION ); if ( false !== strpos( (string) $hook, self::SLUG . '-settings' ) ) { wp_enqueue_media(); } wp_enqueue_script( 'monthwrap-admin', plugins_url( 'assets/admin.js', MONTHWRAP_FILE ), array( 'jquery' ), MONTHWRAP_VERSION, true ); wp_localize_script( 'monthwrap-admin', 'monthwrapAdmin', array( 'chooseLogo' => __( 'Choose a logo', 'monthwrap' ), 'useLogo' => __( 'Use this logo', 'monthwrap' ), ) ); } /** * A class on Monthwrap screens' body, for their background. * * @param string $classes Space-separated classes. * @return string */ public static function body_class( $classes ) { global $hook_suffix; return self::ours( $hook_suffix ) ? $classes . ' mw-screen' : $classes; } /** * The branded header shared by every Monthwrap screen. * * @param string $current overview, activity, reports or settings. */ public static function header( $current ) { $tabs = array( 'overview' => __( 'Overview', 'monthwrap' ), 'activity' => __( 'Activity', 'monthwrap' ), 'reports' => __( 'Reports', 'monthwrap' ), 'settings' => __( 'Settings', 'monthwrap' ), ); ?>
monthwrap
'array', 'sanitize_callback' => array( __CLASS__, 'sanitize_report' ), 'default' => array(), 'show_in_rest' => false, ) ); register_setting( self::SLUG, Monthwrap_Forms::OPTION, array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_checkbox' ), 'default' => '1', 'show_in_rest' => false, ) ); register_setting( self::SLUG . '_connection', self::KEY_OPTION, array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_key' ), 'default' => '', 'show_in_rest' => false, ) ); } /** * Report branding: agency name, logo, colour, and what reports show. * * @param mixed $value Submitted value. * @return array */ public static function sanitize_report( $value ) { $value = is_array( $value ) ? $value : array(); $logo = absint( $value['logo_id'] ?? 0 ); $color = sanitize_hex_color( '#' . ltrim( trim( (string) ( $value['color'] ?? '' ) ), '#' ) ); return array( 'agency_name' => sanitize_text_field( (string) ( $value['agency_name'] ?? '' ) ), 'logo_id' => $logo && wp_attachment_is_image( $logo ) ? $logo : 0, 'color' => $color ? strtolower( $color ) : '#4f46e5', 'lead_names' => ! empty( $value['lead_names'] ), 'credit' => ! empty( $value['credit'] ), ); } /** * Checks a pasted key. Blank keeps the saved one. A key Monthwrap refuses isn't saved. * * @param mixed $value Submitted value. * @return string */ public static function sanitize_key( $value ) { $saved = Monthwrap_Api::site_key(); $value = trim( sanitize_text_field( (string) $value ) ); if ( '' === $value || $value === $saved ) { return $saved; } if ( ! preg_match( '/^mw_site_[A-Za-z0-9_-]{20,}$/', $value ) ) { add_settings_error( self::KEY_OPTION, 'monthwrap_key_format', __( 'That doesn\'t look like a Monthwrap site key. Keys start with mw_site_. Copy it again from Monthwrap.', 'monthwrap' ) ); return $saved; } $reply = Monthwrap_Api::whoami( $value ); if ( 401 === $reply['code'] ) { add_settings_error( self::KEY_OPTION, 'monthwrap_key_refused', __( 'Monthwrap didn\'t accept that key. It may have been replaced; ask your agency for the current one.', 'monthwrap' ) ); return $saved; } if ( 200 !== $reply['code'] ) { add_settings_error( self::KEY_OPTION, 'monthwrap_key_unchecked', __( 'Saved, but Monthwrap couldn\'t be reached to check the key. The plugin will keep trying.', 'monthwrap' ), 'warning' ); } else { add_settings_error( self::KEY_OPTION, 'monthwrap_key_saved', __( 'Connected. This site now reports to Monthwrap.', 'monthwrap' ), 'success' ); } return $value; } /** * A checkbox's value: "1" when ticked, "0" when not. * * @param mixed $value Submitted value. * @return string */ public static function sanitize_checkbox( $value ) { return '1' === (string) $value ? '1' : '0'; } /** * A new key was saved: note when the site connected, forget the old status and say hello * straight away. */ public static function key_changed() { delete_transient( self::WHO_CACHE ); delete_option( Monthwrap_Sender::STATUS ); if ( Monthwrap_Api::site_key() ) { add_option( Monthwrap_Sender::CONNECTED_AT, time(), '', false ); Monthwrap_Sender::send( true ); } } /** * "Send a test": a heartbeat plus anything waiting, then back with the result. */ public static function test_connection() { self::check_request( 'monthwrap_test' ); delete_transient( self::WHO_CACHE ); $reply = Monthwrap_Sender::send( true ); $ok = is_array( $reply ) && 200 === $reply['code']; $error = Monthwrap_Sender::status()['last_error']; if ( $ok ) { $message = __( 'Monthwrap received this site\'s check-in.', 'monthwrap' ); } elseif ( '' !== $error ) { $message = $error; } else { $message = __( 'The test didn\'t go through.', 'monthwrap' ); } self::flash( $message, $ok ? 'success' : 'error' ); wp_safe_redirect( self::url( 'settings' ) ); exit; } /** * Removes the key. Nothing more is sent until a key is added again; the activity log and * reports carry on as before. */ public static function disconnect() { self::check_request( 'monthwrap_disconnect' ); delete_option( self::KEY_OPTION ); // Not update_option: a blank key means "keep" there. delete_option( Monthwrap_Sender::CONNECTED_AT ); delete_transient( self::WHO_CACHE ); delete_option( Monthwrap_Sender::STATUS ); wp_clear_scheduled_hook( Monthwrap_Sender::HOOK ); self::flash( __( 'Disconnected from Monthwrap. Activity is still recorded on this site.', 'monthwrap' ), 'success' ); wp_safe_redirect( self::url( 'settings' ) ); exit; } /** * The report as a web page. */ public static function report_view() { self::check_request( 'monthwrap_report' ); nocache_headers(); header( 'Content-Type: text/html; charset=utf-8' ); echo Monthwrap_Pdf::html( self::requested_report() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- a complete document from templates/report.php, which escapes every value. exit; } /** * The report as a PDF download. */ public static function report_pdf() { self::check_request( 'monthwrap_report' ); $report = self::requested_report(); $pdf = Monthwrap_Pdf::pdf( $report ); nocache_headers(); header( 'Content-Type: application/pdf' ); header( 'Content-Disposition: attachment; filename="' . Monthwrap_Pdf::filename( $report ) . '"' ); header( 'Content-Length: ' . strlen( $pdf ) ); echo $pdf; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- binary PDF file. exit; } /** * A link to the report for a month, as a page or a PDF. * * @param string $month "YYYY-MM". * @param string $as "view" or "pdf". * @return string */ public static function report_url( $month, $as = 'pdf' ) { return wp_nonce_url( add_query_arg( array( 'action' => 'pdf' === $as ? 'monthwrap_report_pdf' : 'monthwrap_report_view', 'month' => $month, ), admin_url( 'admin-post.php' ) ), 'monthwrap_report' ); } /** * The report for the month in the request (this month if none). * * @return array */ private static function requested_report() { $month = isset( $_GET['month'] ) ? sanitize_text_field( wp_unslash( $_GET['month'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checked in check_request(). if ( ! preg_match( '/^\d{4}-\d{2}$/', $month ) ) { $month = wp_date( 'Y-m' ); } list( $start, $end ) = Monthwrap_Report::month_range( $month ); return Monthwrap_Report::build( $start, $end ); } /** * Stops unless the user may manage the site and the request carries the right nonce. * * @param string $action Nonce action. */ private static function check_request( $action ) { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'monthwrap' ), 403 ); } check_admin_referer( $action ); } /** * A one-time message for the next screen this user sees. * * @param string $message The message. * @param string $type success, warning or error. */ private static function flash( $message, $type ) { set_transient( 'monthwrap_flash_' . get_current_user_id(), array( 'message' => $message, 'type' => $type, ), MINUTE_IN_SECONDS ); } /** * Shows and clears the one-time message, and the Settings API's messages. */ public static function messages() { echo '
'; settings_errors(); $flash = get_transient( 'monthwrap_flash_' . get_current_user_id() ); if ( is_array( $flash ) ) { delete_transient( 'monthwrap_flash_' . get_current_user_id() ); printf( '

%s

', esc_attr( $flash['type'] ), esc_html( $flash['message'] ) ); } } /** * A warning on the Plugins screen if Monthwrap stops accepting the site key. Nowhere * else, and nothing when the site isn't connected: the plugin works without Monthwrap. */ public static function notices() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'plugins' !== $screen->id || ! current_user_can( 'manage_options' ) ) { return; } if ( Monthwrap_Api::site_key() && Monthwrap_Sender::status()['key_refused'] ) { printf( '

%s %s

', esc_html__( 'Monthwrap stopped accepting this site\'s key, so activity isn\'t reaching your agency\'s Monthwrap account.', 'monthwrap' ), esc_url( self::url( 'settings' ) ), esc_html__( 'Fix it', 'monthwrap' ) ); } } /** * Who the saved key belongs to, cached for ten minutes. * * @return array|null client_name and agency_name, or null if unknown. */ public static function connected_to() { if ( ! Monthwrap_Api::site_key() ) { return null; } $cached = get_transient( self::WHO_CACHE ); if ( is_array( $cached ) ) { return $cached; } $reply = Monthwrap_Api::whoami(); if ( 200 !== $reply['code'] || ! is_array( $reply['body'] ) ) { return null; } $who = array( 'client_name' => (string) ( $reply['body']['client_name'] ?? '' ), 'agency_name' => (string) ( $reply['body']['agency_name'] ?? '' ), ); set_transient( self::WHO_CACHE, $who, 10 * MINUTE_IN_SECONDS ); return $who; } /** * A time in the site's time zone and formats. * * @param string $iso An ISO 8601 time. * @return string */ public static function when( $iso ) { $timestamp = strtotime( (string) $iso ); return $timestamp ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp ) : ''; } }