requests = new DOCSINO_Admin_Requests(); $this->templates = new DOCSINO_Admin_Templates(); $this->settings = new DOCSINO_Admin_Settings(); $this->setup = new DOCSINO_Admin_Setup(); $this->pro_page = new DOCSINO_Admin_Pro_Page(); add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_filter( 'admin_body_class', array( $this, 'body_class' ) ); add_action( 'in_admin_header', array( $this, 'app_bar' ), 1000 ); add_filter( 'plugin_action_links_' . plugin_basename( DOCSINO_PLUGIN_FILE ), array( $this, 'action_links' ) ); add_filter( 'plugin_row_meta', array( $this, 'row_meta' ), 10, 2 ); add_action( 'admin_post_docsino_review_prompt', array( $this, 'handle_review_prompt' ) ); add_action( 'admin_post_docsino_toggle_wp_menu', array( $this, 'handle_toggle_wp_menu' ) ); add_action( 'admin_post_docsino_dismiss_exit_hint', array( $this, 'handle_dismiss_exit_hint' ) ); add_action( 'admin_notices', array( $this, 'start_notice_capture' ), PHP_INT_MIN ); add_action( 'all_admin_notices', array( $this, 'end_notice_capture' ), PHP_INT_MAX ); } /** * Whether the current user has chosen to see the WordPress menu on DocsInOrder screens. * Managers never do: WordPress has nothing else for them. * * @return bool */ public static function show_wp_menu() { return ! DOCSINO_Roles::is_manager_only() && (bool) get_user_meta( get_current_user_id(), 'docsino_show_wp_menu', true ); } /** * Start collecting notices from WordPress and other plugins on DocsInOrder screens. */ public function start_notice_capture() { if ( self::is_own_screen() && ! self::is_freemius_screen() ) { ob_start(); } } /** * Discard the collected notices. DocsInOrder screens stay clean; the * notices still appear on every other WordPress screen. DocsInOrder's * own messages are printed inside its pages, so they aren't affected. */ public function end_notice_capture() { if ( self::is_own_screen() && ! self::is_freemius_screen() ) { ob_end_clean(); } } /** * Turn the WordPress menu on or off on DocsInOrder screens (per user). */ public function handle_toggle_wp_menu() { self::authorize( 'docsino_toggle_wp_menu' ); update_user_meta( get_current_user_id(), 'docsino_show_wp_menu', self::show_wp_menu() ? 0 : 1 ); self::redirect( wp_get_referer() ? wp_get_referer() : self::page_url() ); } /** * Hide the one-time "back to WordPress" hint. */ public function handle_dismiss_exit_hint() { self::authorize( 'docsino_dismiss_exit_hint' ); update_user_meta( get_current_user_id(), 'docsino_seen_exit_hint', 1 ); self::redirect( wp_get_referer() ? wp_get_referer() : self::page_url() ); } /** * Add the top-level menu and its submenus. */ public function register_menu() { $cap = docsino_capability(); add_menu_page( __( 'DocsInOrder', 'docs-in-order' ), __( 'DocsInOrder', 'docs-in-order' ), $cap, 'docs-in-order', array( $this->requests, 'render_page' ), self::menu_icon(), 26 ); add_submenu_page( 'docs-in-order', __( 'Document requests', 'docs-in-order' ), __( 'Requests', 'docs-in-order' ), $cap, 'docs-in-order', array( $this->requests, 'render_page' ) ); add_submenu_page( 'docs-in-order', __( 'Templates', 'docs-in-order' ), __( 'Templates', 'docs-in-order' ), $cap, 'docs-in-order-templates', array( $this->templates, 'render_page' ) ); add_submenu_page( 'docs-in-order', __( 'Settings', 'docs-in-order' ), __( 'Settings', 'docs-in-order' ), 'manage_options', 'docs-in-order-settings', array( $this->settings, 'render_page' ) ); add_submenu_page( 'docs-in-order', __( 'Set up DocsInOrder', 'docs-in-order' ), __( 'Setup', 'docs-in-order' ), 'manage_options', 'docs-in-order-setup', array( $this->setup, 'render_page' ) ); add_submenu_page( 'docs-in-order', __( 'DocsInOrder Pro', 'docs-in-order' ), 'active' === DOCSINO_Admin_Pro_Page::state() ? __( 'Pro', 'docs-in-order' ) : ( 'inactive' === DOCSINO_Admin_Pro_Page::state() ? __( 'Activate Pro', 'docs-in-order' ) : __( 'Get Pro', 'docs-in-order' ) ), 'manage_options', DOCSINO_Admin_Pro_Page::PAGE, array( $this->pro_page, 'render_page' ) ); } /** * Whether the current admin screen is one of DocsInOrder's. * * @return bool */ public static function is_own_screen() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the current page only. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; return 0 === strpos( $page, 'docs-in-order' ); } /** * Whether the current screen is one of Freemius's pages under our menu * (Account, Add-ons). Their licence messages must stay visible there. * * @return bool */ public static function is_freemius_screen() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the current page only. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; return in_array( $page, array( 'docs-in-order-pro', 'docs-in-order-account', 'docs-in-order-addons', 'docs-in-order-pricing', 'docs-in-order-contact', 'docs-in-order-affiliation', 'docs-in-order-wp-support-forum' ), true ); } /** * URL of the in-plugin Add-Ons page (DocsInOrder Pro), if Freemius is available. * * @return string */ public static function addons_url() { return self::page_url( DOCSINO_Admin_Pro_Page::PAGE ); } /** * A small, inline "Pro can do this" hint, shown only when Pro's feature isn't active. * * @param string $feature Feature key. * @param string $text Hint text. */ public static function pro_hint( $feature, $text ) { if ( docsino_has_feature( $feature ) || ! current_user_can( 'manage_options' ) ) { return; } printf( '

%1$s %2$s %4$s

', esc_html__( 'Pro', 'docs-in-order' ), esc_html( $text ), esc_url( self::addons_url() ), 'inactive' === DOCSINO_Admin_Pro_Page::state() ? esc_html__( 'Activate your licence', 'docs-in-order' ) : esc_html__( 'Learn more', 'docs-in-order' ) ); } /** * Load admin CSS and JS on DocsInOrder screens only. * * @param string $hook Current admin page hook. */ public function enqueue( $hook ) { if ( false === strpos( (string) $hook, 'docs-in-order' ) ) { return; } $deps = array(); if ( false !== strpos( (string) $hook, 'docs-in-order-settings' ) || false !== strpos( (string) $hook, 'docs-in-order-setup' ) ) { // The logo picker uses the Media Library; load it before our script. wp_enqueue_media(); $deps[] = 'media-editor'; } wp_enqueue_style( 'docsino-admin', DOCSINO_PLUGIN_URL . 'assets/css/admin.css', array(), docsino_asset_version( 'assets/css/admin.css' ) ); wp_enqueue_script( 'docsino-admin', DOCSINO_PLUGIN_URL . 'assets/js/admin.js', $deps, docsino_asset_version( 'assets/js/admin.js' ), true ); } /** * Mark DocsInOrder screens for the full-page layout. * * @param string $classes Body classes. * @return string */ public function body_class( $classes ) { if ( ! self::is_own_screen() ) { return $classes; } return $classes . ' dio-app' . ( self::show_wp_menu() ? ' dio-app--wp-menu' : '' ); } /** * Print the DocsInOrder app bar. */ public function app_bar() { if ( ! self::is_own_screen() ) { return; } $manager = DOCSINO_Roles::is_manager_only(); $show_menu = self::show_wp_menu(); $show_hint = ! $manager && ! get_user_meta( get_current_user_id(), 'docsino_seen_exit_hint', true ); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Highlighting the current tab only. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : ''; // phpcs:enable $tabs = array( 'docs-in-order' => __( 'Requests', 'docs-in-order' ) ); $tabs['docs-in-order-templates'] = __( 'Templates', 'docs-in-order' ); if ( current_user_can( 'manage_options' ) ) { $tabs['docs-in-order-settings'] = __( 'Settings', 'docs-in-order' ); $checks = DOCSINO_Health::checks(); $todo = count( $checks ) - DOCSINO_Health::passed( $checks ); /* translators: %d: number of setup items still to do. */ $tabs['docs-in-order-setup'] = $todo ? sprintf( __( 'Setup (%d to do)', 'docs-in-order' ), $todo ) : __( 'Setup', 'docs-in-order' ); // Before Pro: "Get Pro". Pro installed without a licence: "Activate Pro" (highlighted). // With Pro: the licence page (Account), when Freemius has one. $pro_state = DOCSINO_Admin_Pro_Page::state(); if ( 'active' === $pro_state && docsino_freemius_ready() && docsino_fs()->is_registered() ) { $tabs['docs-in-order-account'] = __( 'Pro', 'docs-in-order' ); } elseif ( 'inactive' === $pro_state ) { $tabs[ DOCSINO_Admin_Pro_Page::PAGE ] = __( 'Activate Pro', 'docs-in-order' ); $attention = DOCSINO_Admin_Pro_Page::PAGE; } else { $tabs[ DOCSINO_Admin_Pro_Page::PAGE ] = 'active' === $pro_state ? __( 'Pro', 'docs-in-order' ) : __( 'Get Pro', 'docs-in-order' ); } } /** * App bar tabs: page slug => label. DocsInOrder Pro adds "Team". * * @param array $tabs Tabs. */ $tabs = (array) apply_filters( 'docsino_app_bar_tabs', $tabs ); ?>
DocsInOrder
review_prompt(); } /** * One-time "Enjoying DocsInOrder?" prompt after a few completed requests. */ private function review_prompt() { if ( ! current_user_can( 'manage_options' ) ) { return; } $state = get_user_meta( get_current_user_id(), 'docsino_review_prompt', true ); if ( 'done' === $state || ( is_numeric( $state ) && (int) $state > time() ) ) { return; } if ( self::completed_requests() < self::REVIEW_PROMPT_AFTER ) { return; } ?>

get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE status = 'completed'", $table ) ); } /** * "Setup" and "Settings" links under the plugin name on the Plugins screen. * * @param string[] $links Action links. * @return string[] */ public function action_links( $links ) { if ( current_user_can( 'manage_options' ) ) { array_unshift( $links, '' . esc_html__( 'Setup', 'docs-in-order' ) . '', '' . esc_html__( 'Settings', 'docs-in-order' ) . '' ); } return $links; } /** * "Get help" and "Rate ★★★★★" next to "View details" on the Plugins screen. * * @param string[] $meta Row meta links. * @param string $file Plugin file. * @return string[] */ public function row_meta( $meta, $file ) { if ( plugin_basename( DOCSINO_PLUGIN_FILE ) !== $file ) { return $meta; } $meta[] = '' . esc_html__( 'Get help', 'docs-in-order' ) . ''; $meta[] = '' . esc_html__( 'Rate', 'docs-in-order' ) . ' '; return $meta; } /** * The admin menu icon: logo A in one colour, as a data URI. * WordPress recolours single-colour SVG menu icons to match the admin colour scheme. * * @return string */ public static function menu_icon() { $svg = ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Data URI for the menu icon. return 'data:image/svg+xml;base64,' . base64_encode( $svg ); } /** * The full-colour logo icon (direction A) as inline SVG. * * @param int $size Width and height in pixels. * @return string */ public static function logo_svg( $size = 32, $light = false ) { if ( $light ) { // For dark (teal) backgrounds. return sprintf( '', (int) $size ); } return sprintf( '', (int) $size ); } /** * URL of a DocsInOrder admin screen. * * @param string $page Page slug. * @param array $args Extra query args. * @return string */ public static function page_url( $page = 'docs-in-order', array $args = array() ) { return add_query_arg( array_merge( array( 'page' => $page ), $args ), admin_url( 'admin.php' ) ); } /** * URL of a request's review screen. * * @param int $checklist_id Request ID. * @param array $args Extra query args. * @return string */ public static function review_url( $checklist_id, array $args = array() ) { return self::page_url( 'docs-in-order', array_merge( array( 'view' => 'request', 'id' => (int) $checklist_id, ), $args ) ); } /** * Admin-only URL that downloads a stored client file. * * @param int $upload_id Upload ID. * @return string */ public static function download_url( $upload_id ) { return self::file_url( 'docsino_download', $upload_id ); } /** * Admin-only URL that shows a stored PDF or image in the browser. * * @param int $upload_id Upload ID. * @return string */ public static function view_url( $upload_id ) { return self::file_url( 'docsino_view', $upload_id ); } /** * Nonce-protected admin-post URL for a file action. * * @param string $action docsino_download or docsino_view. * @param int $upload_id Upload ID. * @return string */ private static function file_url( $action, $upload_id ) { return wp_nonce_url( add_query_arg( array( 'action' => $action, 'upload_id' => (int) $upload_id, ), admin_url( 'admin-post.php' ) ), $action . '_' . (int) $upload_id ); } /** * Opening tag and hidden fields of a form that posts to admin-post.php. * * @param string $action admin-post action. * @param string $nonce_action Nonce action. * @param array $fields Extra hidden fields. * @param string $class Form CSS class. */ public static function form_open( $action, $nonce_action, array $fields = array(), $class = '' ) { printf( '
', esc_url( admin_url( 'admin-post.php' ) ), esc_attr( $class ) ); printf( '', esc_attr( $action ) ); foreach ( $fields as $name => $value ) { printf( '', esc_attr( $name ), esc_attr( (string) $value ) ); } wp_nonce_field( $nonce_action ); } /** * Stop unless the user has the capability and the nonce is valid. * * @param string $nonce_action Nonce action. * @param string|null $capability Capability (default: the DocsInOrder capability). */ public static function authorize( $nonce_action, $capability = null ) { if ( ! current_user_can( $capability ? $capability : docsino_capability() ) ) { wp_die( esc_html__( 'You are not allowed to do that.', 'docs-in-order' ), 403 ); } check_admin_referer( $nonce_action ); } /** * Redirect with notices, then stop. * * @param string $url Destination. * @param string|string[] $notices Notice keys. */ public static function redirect( $url, $notices = array() ) { $notices = array_filter( (array) $notices ); if ( $notices ) { $hash = ''; $pos = strpos( $url, '#' ); if ( false !== $pos ) { $hash = substr( $url, $pos ); $url = substr( $url, 0, $pos ); } $url = add_query_arg( 'docsino_notice', implode( ',', $notices ), $url ) . $hash; } wp_safe_redirect( $url ); exit; } /** * Remember an error message for the next screen and redirect there. * * @param string $message Error message. * @param string $url Destination. * @param string|string[] $notices Other notices to show as well. */ public static function fail( $message, $url, $notices = array() ) { self::remember_error( $message ); self::redirect( $url, array_merge( (array) $notices, array( 'error' ) ) ); } /** * Store an error message shown by the "error" notice. * * @param string $message Error message. */ public static function remember_error( $message ) { set_transient( 'docsino_admin_error_' . get_current_user_id(), $message, 60 ); } /** * Print the notices named in the URL. */ public static function render_notices() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only; maps to fixed messages. $raw = isset( $_GET['docsino_notice'] ) ? sanitize_text_field( wp_unslash( $_GET['docsino_notice'] ) ) : ''; $keys = array_filter( array_map( 'sanitize_key', explode( ',', $raw ) ) ); $messages = array( 'created' => array( 'success', __( 'Request created. Copy the client link on the right and send it to your client.', 'docs-in-order' ) ), 'created_emailed' => array( 'success', __( 'Request created and emailed to your client.', 'docs-in-order' ) ), 'template_saved_too' => array( 'success', __( 'The checklist was also saved as a template.', 'docs-in-order' ) ), 'template_not_saved' => array( 'warning', __( 'The checklist could not be saved as a template.', 'docs-in-order' ) ), 'emailed_request' => array( 'success', __( 'Request email sent to your client.', 'docs-in-order' ) ), 'emailed_reminder' => array( 'success', __( 'Reminder sent to your client.', 'docs-in-order' ) ), 'regenerated' => array( 'success', __( 'New link created. The old link no longer works.', 'docs-in-order' ) ), 'revoked' => array( 'warning', __( 'Link revoked. The client can no longer open it.', 'docs-in-order' ) ), 'extended' => array( 'success', __( 'Link expiry date updated. The link itself hasn\'t changed.', 'docs-in-order' ) ), 'approved' => array( 'success', __( 'Item approved.', 'docs-in-order' ) ), 'rejected' => array( 'success', __( 'Changes requested. Your client will see your note the next time they open their link.', 'docs-in-order' ) ), 'undone' => array( 'success', __( 'Decision undone.', 'docs-in-order' ) ), 'file_deleted' => array( 'success', __( 'File deleted.', 'docs-in-order' ) ), 'completed' => array( 'success', __( 'Request marked as complete. The client can still view it but can\'t upload more files.', 'docs-in-order' ) ), 'reopened' => array( 'success', __( 'Request reopened.', 'docs-in-order' ) ), 'archived' => array( 'success', __( 'Request archived. The client link no longer works.', 'docs-in-order' ) ), 'request_deleted' => array( 'success', __( 'Request and all its files deleted.', 'docs-in-order' ) ), 'template_saved' => array( 'success', __( 'Template saved.', 'docs-in-order' ) ), 'template_duplicated' => array( 'success', __( 'Template copied. You can now edit your copy.', 'docs-in-order' ) ), 'template_deleted' => array( 'success', __( 'Template deleted.', 'docs-in-order' ) ), 'template_not_deleted' => array( 'error', __( 'That template can\'t be deleted.', 'docs-in-order' ) ), 'settings_saved' => array( 'success', __( 'Settings saved.', 'docs-in-order' ) ), 'emails_reset' => array( 'success', __( 'Email wording restored to the defaults.', 'docs-in-order' ) ), 'test_email_sent' => array( 'success', __( 'Test email sent. Check your inbox (and spam folder), then answer below.', 'docs-in-order' ) ), 'setup_branding_saved' => array( 'success', __( 'Firm name and logo saved.', 'docs-in-order' ) ), 'missing' => array( 'error', __( 'That item could not be found.', 'docs-in-order' ) ), ); /** * Notice messages, keyed by notice key: array( type, text ). Pro adds its own. * * @param array $messages Messages. */ $messages = (array) apply_filters( 'docsino_admin_notice_messages', $messages ); if ( in_array( 'error', $keys, true ) ) { $transient = 'docsino_admin_error_' . get_current_user_id(); $message = get_transient( $transient ); delete_transient( $transient ); $messages['error'] = array( 'error', $message ? $message : __( 'Something went wrong.', 'docs-in-order' ) ); } foreach ( $keys as $key ) { if ( isset( $messages[ $key ] ) ) { printf( '

%2$s

', esc_attr( $messages[ $key ][0] ), esc_html( $messages[ $key ][1] ) ); } } } /** * Whether the current user may open a request. Everyone with DocsInOrder * access may, unless an add-on narrows it (DocsInOrder Pro's "Only their requests"). * * @param object $checklist Request. * @return bool */ public static function can_access_request( $checklist ) { /** * Whether a user may open a request. * * @param bool $can Default true. * @param object $checklist Request. * @param int $user_id User. */ return (bool) apply_filters( 'docsino_user_can_access_request', true, $checklist, get_current_user_id() ); } /** * Stop unless the current user may open the request. * * @param object $checklist Request. */ public static function require_request_access( $checklist ) { if ( ! self::can_access_request( $checklist ) ) { wp_die( esc_html__( 'You don\'t have access to this request.', 'docs-in-order' ), 403 ); } } /** * Whether the current user may create, edit, copy and delete templates * (everyone with access can always use templates). * * @return bool */ public static function can_manage_templates() { /** * Whether a user may create, edit, copy and delete templates. Default true for everyone with access. * * @param bool $can Default true. * @param int $user_id User. */ return (bool) apply_filters( 'docsino_user_can_manage_templates', true, get_current_user_id() ); } /** * Whether the current user may delete requests and files (on this request). * * @param object $checklist Request. * @return bool */ public static function can_delete( $checklist ) { /** * Whether a user may delete a request or its files. Default true for everyone with access. * * @param bool $can Default true. * @param object $checklist Request. * @param int $user_id User. */ return (bool) apply_filters( 'docsino_user_can_delete', true, $checklist, get_current_user_id() ); } /** * A status pill. * * @param string $key Style key. * @param string $label Text. * @return string HTML. */ public static function pill( $key, $label ) { return '' . esc_html( $label ) . ''; } /** * A progress bar with an accessible label. * * @param int $done Items received. * @param int $total Total items. * @return string HTML. */ public static function progress_bar( $done, $total ) { $percent = $total > 0 ? (int) round( 100 * $done / $total ) : 0; return sprintf( '%2$d/%1$d', (int) $total, (int) $done, $percent, esc_attr__( 'Progress', 'docs-in-order' ) ); } /** * Local date (and optionally time) for a UTC MySQL datetime. * * @param string|null $utc UTC datetime. * @param bool $with_time Include the time. * @return string */ public static function date( $utc, $with_time = false ) { if ( empty( $utc ) ) { return '—'; } $format = get_option( 'date_format' ) . ( $with_time ? ' ' . get_option( 'time_format' ) : '' ); return wp_date( $format, strtotime( $utc . ' UTC' ) ); } }