'; switch ( $kind ) { case 'blocking': return $open . ''; case 'verify': return $open . ''; case 'passed': return $open . ''; case 'unchecked': return $open . ''; default: return $open . ''; } } /** * Plugin page URL. * * @param array $args Extra query args. * @return string */ public static function url( array $args = array() ) { return add_query_arg( array_merge( array( 'page' => 'shouldcheck' ), $args ), admin_url( 'tools.php' ) ); } /** * Page heading with breadcrumb. * * @param string $title Heading. * @param string $crumb Breadcrumb text. * @return string */ public static function heading( $title, $crumb = '' ) { $out = '
'; if ( '' !== $crumb ) { $out .= '

' . esc_html( $crumb ) . '

'; } return $out . '

' . esc_html( $title ) . '

'; } /** * Tab navigation. * * @param string $current overview|reviews|changes. * @return string */ public static function tabs( $current ) { $tabs = array( 'overview' => 'Overview', 'reviews' => 'Reviews', 'changes' => 'Site changes', ); $out = ''; } /** * Package header card. * * @param array $report Report. * @param array $extra Extra lines: array of plain strings. * @param array $a Assessment. * * @return string */ public static function package_card( array $report, array $extra = array(), array $a = array() ) { $m = (array) ( $report['metadata'] ?? array() ); $env = (array) ( $report['environment'] ?? array() ); $out = '

' . esc_html( $m['name'] ?? 'Unidentified package' ) . '

'; if ( ! empty( $m['version'] ) ) { $out .= '' . esc_html( $m['version'] ) . ''; } if ( ! empty( $a['money_path'] ) ) { $out .= '' . esc_html( ShouldCheck_Assessment::money_label( (string) $a['money_path'] ) ) . ''; } $out .= '
'; foreach ( $extra as $line ) { $out .= '

' . $line . '

'; // Callers escape. } if ( $env ) { $out .= '

Reviewed against WordPress ' . esc_html( $env['wordpress'] ?? '?' ) . ' · PHP ' . esc_html( $env['php'] ?? '?' ) . ' · WooCommerce ' . esc_html( ! empty( $env['woocommerce'] ) ? $env['woocommerce'] : 'not active' ) . ' · ' . esc_html( $env['theme'] ?? '' ) . '

'; } if ( ! empty( $a['money_path'] ) ) { $out .= '

' . esc_html( $a['money_path'] ) . ' If it breaks, customers may not be able to pay.' . self::quiet_sentence() . '

'; } if ( ! empty( $a['capabilities'] ) ) { $out .= self::about( (array) $a['capabilities'] ); } if ( isset( $a['headline'] ) ) { $out .= '
'; } return $out . '
'; } /** * The assessment as separate parts, so a screen can put actions between them. * * @param array $a Output of ShouldCheck_Assessment::build(). * @return array{verdict:string,findings:string,rest:string} */ public static function parts( array $a ) { $findings = '
' . self::grouped_findings( (array) $a['blocking'], (array) $a['verify'] ) . '
'; $rest = '
'; if ( ! empty( $a['not_checked'] ) ) { $n = count( $a['not_checked'] ); $rest .= self::group( 'shouldcheck-g-unchecked', 'unchecked', $n . ' not checked', self::item_list( 'unchecked', 'Not checked', $a['not_checked'] ), false ); } if ( ! empty( $a['notes'] ) ) { $n = count( $a['notes'] ); $rest .= self::group( 'shouldcheck-g-notes', 'info', $n . ' good to know', self::item_list( 'info', 'Good to know', $a['notes'] ) . '

These are not problems. They are things to know before you decide. They did not change the verdict.

', false ); } if ( $a['passed'] ) { $n = count( $a['passed'] ); $rest .= self::group( 'shouldcheck-g-passed', 'passed', $n . ' check' . ( 1 === $n ? '' : 's' ) . ' passed', self::item_list( 'passed', '', $a['passed'], true ) . '

A pass means a declaration or code pattern was found and matches this site, or a name comparison found no match. It is not a test of the plugin.

', false ); } if ( $a['plan'] ) { $rest .= self::plan( $a['plan'] ); } $rest .= self::exposes( (array) ( $a['exposes'] ?? array() ) ); $rest .= self::limits( $a['limits'] ); $rest .= '
'; return array( 'verdict' => self::verdict( $a ), 'findings' => $findings, 'rest' => $rest, ); } /** * The full assessment in reading order: verdict, what needs attention, detail. * * @param array $a Output of ShouldCheck_Assessment::build(). * @return string */ public static function assessment( array $a ) { $p = self::parts( $a ); return '
' . $p['verdict'] . $p['findings'] . $p['rest'] . '
'; } /** * Verdict card. * * @param array $a Assessment. */ private static function verdict( array $a ) { $kind = array( 'blocked' => 'blocking', 'attention' => 'passed', 'clear' => 'passed', 'unknown' => 'unchecked', 'neutral' => 'info', ); $out = '
'; $out .= '
' . self::icon( $kind[ $a['tone'] ] ?? 'info' ) . '

' . esc_html( $a['headline'] ) . '

'; if ( '' !== $a['summary'] ) { $out .= '

' . esc_html( $a['summary'] ) . '

'; } $out .= '
'; if ( ! empty( $a['pending'] ) ) { $out .= '

Still comparing with your active plugins. This page continues by itself.

'; } $out .= self::tiles( (array) ( $a['tiles'] ?? array() ) ); if ( 'unknown' !== $a['tone'] ) { $out .= self::steps( (array) ( $a['steps'] ?? array() ), (string) $a['recommendation'], (string) ( $a['kind'] ?? 'zip' ) ); } $out .= self::proof_strip( (array) ( $a['read'] ?? array() ) ); return $out . '
'; } /** * The items of a list. * * @param string $icon Icon kind. * @param string $label Status word, or '' for none. * @param array $items Items. * @param bool $tight Compact rows. * @return string */ private static function item_list( $icon, $label, array $items, $tight = false ) { $out = '
    '; foreach ( $items as $it ) { $out .= '
  • ' . self::icon( $icon ) . '
    ' . esc_html( $it['title'] ) . ''; if ( ! empty( $it['tag'] ) ) { $out .= ' ' . esc_html( $it['tag'] ) . ''; } if ( '' !== $label ) { $out .= '' . esc_html( $label ) . ''; } $out .= '
    '; $out .= self::finding_body( $it, 'Next' ); $out .= '
  • '; } return $out . '
'; } /** * A collapsed group with a count in its summary. * * @param string $id Element id. * @param string $icon Icon kind. * @param string $title Summary text (already includes the count). * @param string $inner Body HTML. * @param bool $open Whether it starts open. * @return string */ private static function group( $id, $icon, $title, $inner, $open ) { return '
' . self::icon( $icon ) . '' . esc_html( $title ) . '' . $inner . '
'; } /** * What was done, in plain words, and by whom. * * @param array $plan Plan items with their states. */ private static function plan( array $plan ) { $labels = array( 'passed' => 'Passed', 'failed' => 'Failed', 'attention' => 'Needs a look', 'done' => 'Done', 'not_run' => 'Not run', 'not_yet' => 'Not automated yet', 'manual' => 'You do this', ); $icons = array( 'passed' => 'passed', 'done' => 'passed', 'failed' => 'blocking', 'attention' => 'verify', ); $inner = '
    '; $manual = array(); foreach ( $plan as $p ) { if ( 'manual' === $p['state'] ) { $manual[] = $p; continue; } $inner .= '
  • ' . self::icon( $icons[ $p['state'] ] ?? 'unchecked' ) . '
    ' . esc_html( $p['title'] ) . ' ' . esc_html( $labels[ $p['state'] ] ?? $p['state'] ) . ''; if ( '' !== $p['note'] ) { $inner .= ' ' . esc_html( $p['note'] ) . ''; } $inner .= '

    ' . esc_html( $p['detail'] ) . '

  • '; } $inner .= '
'; if ( $manual ) { $inner .= '

Left to you (' . count( $manual ) . ')

ShouldCheck does not run plugins, so these need a staging copy of your site.

    '; foreach ( $manual as $p ) { $inner .= '
  • ' . esc_html( $p['title'] ) . '. ' . esc_html( $p['detail'] ) . '
  • '; } $inner .= '
'; } $auto = count( $plan ) - count( $manual ); return self::group( 'shouldcheck-g-plan', 'info', 'What ShouldCheck did (' . $auto . ') and what it left to you (' . count( $manual ) . ')', $inner, false ); } /** * Singular of a findings label. * * @param string $label Plural label. * @return string */ private static function singular( $label ) { $map = array( 'REST routes' => 'REST route', 'Logged-out actions' => 'Logged-out action', 'WooCommerce callbacks' => 'WooCommerce callback', ); return $map[ $label ] ?? $label; } /** * What ShouldCheck read, as a one-line strip. * * @param array $read Rows of n and label. * @return string */ private static function proof_strip( array $read ) { $files = 0; $plugins = 0; foreach ( $read as $r ) { if ( false !== strpos( $r['label'], 'PHP file' ) ) { $files = (int) $r['n']; } if ( false !== strpos( $r['label'], 'plugin' ) ) { $plugins = (int) $r['n']; } } if ( ! $files ) { return ''; } $txt = 'ShouldCheck read ' . $files . ' PHP file' . ( 1 === $files ? '' : 's' ); if ( $plugins ) { $txt .= ' and compared them with ' . $plugins . ' of your plugin' . ( 1 === $plugins ? '' : 's' ); } return '

' . esc_html( $txt ) . '.

'; } /** * Four result tiles with a status pill. The pill carries a word, not only a colour. * * @param array $tiles Tiles from the assessment. * @return string */ private static function tiles( array $tiles ) { if ( ! $tiles ) { return ''; } $icon = array( 'blocking' => 'blocking', 'verify' => 'verify', 'passed' => 'passed', 'unchecked' => 'unchecked', 'none' => 'info', ); $out = '
'; foreach ( $tiles as $t ) { $st = (string) $t['status']; $out .= '

' . esc_html( $t['title'] ) . '

' . self::icon( $icon[ $st ] ?? 'info' ) . '' . esc_html( $t['pill'] ) . '

' . esc_html( $t['text'] ) . '

'; if ( ! empty( $t['needs'] ) ) { $out .= 'See what to do'; } elseif ( ! empty( $t['unchecked'] ) ) { $out .= 'See why'; } elseif ( ! empty( $t['passed'] ) ) { $out .= 'See what we checked'; } elseif ( ! empty( $t['notes'] ) ) { $out .= 'See what to know'; } $out .= '
'; } return $out . '
'; } /** * Everything that needs a look, grouped by topic instead of one long list. * * @param array $blocking Blocking items. * @param array $verify Items to check. * @return string */ private static function grouped_findings( array $blocking, array $verify ) { if ( ! $blocking && ! $verify ) { return ''; } $topics = array( 'compat' => array( 'Compatibility', 'Does it fit your WordPress, PHP and WooCommerce?' ), 'conflicts' => array( 'Possible clashes', 'Could it fight with a plugin you already use?' ), 'exposure' => array( 'Outside access', 'Which doors does it leave open to visitors and other servers?' ), 'checkout' => array( 'Checkout', 'Could it get in the way of orders and payments?' ), ); $out = '

What to look at ' . ( count( $blocking ) + count( $verify ) ) . '

'; foreach ( $topics as $key => $t ) { $bl = array_values( array_filter( $blocking, function ( $i ) use ( $key ) { return ( $i['group'] ?? 'compat' ) === $key; } ) ); $vf = array_values( array_filter( $verify, function ( $i ) use ( $key ) { return ( $i['group'] ?? 'compat' ) === $key; } ) ); if ( ! $bl && ! $vf ) { continue; } $out .= '

' . esc_html( $t[0] ) . '

' . esc_html( $t[1] ) . '

    '; foreach ( array_merge( $bl, $vf ) as $n => $it ) { $is_block = $n < count( $bl ); $out .= self::fold_item( $it, $is_block ? 'blocking' : 'verify', $is_block ? 'Fix first' : 'Check', $is_block ); } $out .= '
'; } return $out . '
'; } /** * One finding that opens on click. * * @param array $it Item. * @param string $icon Status icon name. * @param string $label Short status label shown in the summary. * @param bool $open Whether the item starts open. */ private static function fold_item( array $it, $icon, $label, $open ) { $out = '
  • ' . self::icon( $icon ) . '' . esc_html( $it['title'] ) . '' . esc_html( $label ) . '
    '; $out .= self::finding_body( $it, 'What to do' ); return $out . '
  • '; } /** * The text of one finding: the plain explanation with the technical detail folded away, * or the technical text alone when there is no explanation. * * @param array $it Item. * @param string $label Label for the action when there is no explanation. * @return string */ private static function finding_body( array $it, $label ) { $say = (array) ( $it['say'] ?? array() ); $out = ''; if ( ! empty( $say['noticed'] ) ) { $out .= '

    ' . esc_html( $say['noticed'] ) . '

    '; if ( '' !== (string) ( $say['why'] ?? '' ) ) { $out .= '

    ' . esc_html( $say['why'] ) . '

    '; } if ( '' !== (string) ( $say['worry_label'] ?? '' ) ) { $out .= '

    How worried to be: ' . esc_html( $say['worry_label'] ) . '

    '; } if ( '' !== (string) ( $say['do'] ?? '' ) ) { $out .= '

    What to do: ' . esc_html( $say['do'] ) . '

    '; } if ( '' !== (string) ( $it['evidence'] ?? '' ) || '' !== (string) ( $it['action'] ?? '' ) ) { $out .= '
    Show details'; if ( '' !== (string) ( $it['evidence'] ?? '' ) ) { $out .= '

    ' . esc_html( $it['evidence'] ) . '

    '; } if ( '' !== (string) ( $it['action'] ?? '' ) ) { $out .= '

    ' . esc_html( $it['action'] ) . '

    '; } $out .= '
    '; } return $out; } if ( '' !== ( $it['evidence'] ?? '' ) ) { $out .= '

    ' . esc_html( $it['evidence'] ) . '

    '; } if ( '' !== ( $it['action'] ?? '' ) ) { $out .= '

    ' . esc_html( $label ) . ': ' . esc_html( $it['action'] ) . '

    '; } return $out; } /** * "Next" as numbered steps. Falls back to the single recommendation. * * @param array $steps Steps. * @param string $fallback Recommendation text. * @param string $kind Review kind. * * @return string */ private static function steps( array $steps, $fallback, $kind = 'zip' ) { $out = '

    Before you ' . esc_html( ShouldCheck_Assessment::before_verb( $kind ) ) . ', do this

    '; if ( $steps ) { $out .= '
      '; foreach ( $steps as $st ) { $out .= '
    1. ' . esc_html( $st['title'] ) . '. ' . esc_html( $st['text'] ) . '
    2. '; } $out .= '
    '; } $pos = strpos( $fallback, 'Not tested:' ); if ( false !== $pos ) { $fallback = substr( $fallback, $pos ); } if ( '' !== $fallback ) { $out .= '

    ' . esc_html( $fallback ) . '

    '; } return $out . '
    '; } /** * What the package exposes: public entry points and outside hosts named in its code. * * @param array $x Output of the assessment's "exposes". * @return string */ private static function exposes( array $x ) { if ( empty( $x['has'] ) ) { return ''; } $out = ''; foreach ( (array) ( $x['kinds'] ?? array() ) as $k ) { $out .= '
    ' . (int) $k['total'] . ' ' . esc_html( 1 === (int) $k['total'] ? self::singular( $k['label'] ) : $k['label'] ) . ', ' . ( $k['checked'] === $k['total'] ? 'all with a security signal near their handler' : (int) $k['checked'] . ' with a security signal near their handler, ' . (int) $k['unchecked'] . ' without' ) . '
      '; foreach ( array_slice( (array) $k['items'], 0, 40, true ) as $name => $ok ) { $out .= '
    • ' . esc_html( (string) $name ) . ' ' . esc_html( $ok ? 'Check found' : 'No check seen' ) . '
    • '; } if ( count( (array) $k['items'] ) > 40 ) { $out .= '
    • and ' . (int) ( count( $k['items'] ) - 40 ) . ' more
    • '; } $out .= '
    '; } $out .= '

    A security signal is a permission, nonce or signature pattern near its own handler. It is a lead from reading the code, not proof that the entry point is protected.

    '; $domains = (array) ( $x['domains'] ?? array() ); if ( $domains ) { $out .= '

    Outside hosts named in the code

    Named in the code. ShouldCheck cannot tell which are contacted, or when.

      '; foreach ( $domains as $d ) { $out .= '
    • ' . esc_html( $d['domain'] ) . ' ' . (int) $d['count'] . ' host' . ( 1 === (int) $d['count'] ? '' : 's' ) . '' . ( ! empty( $d['provider'] ) ? ' Likely the provider' : '' ) . ( ! empty( $d['docs'] ) ? ' Docs link' : '' ) . '
    • '; } $out .= '
    '; } return self::group( 'shouldcheck-g-exposes', 'info', 'What this package exposes', $out, false ); } /** * Capabilities detected in the code. * * @param array $cap Capability view. */ private static function about( array $cap ) { if ( ! $cap['chips'] && ! $cap['signals'] && ! $cap['inferred'] && ! $cap['has_scan'] ) { return ''; } $out = '

    ' . esc_html( $cap['label'] ) . ''; if ( $cap['inferred'] ) { $out .= ' Guessed from the listing\'s name and tags. The code was not read.'; } $out .= '

    '; if ( $cap['chips'] || $cap['builders'] || $cap['forms'] ) { $out .= '

    '; foreach ( array_merge( $cap['chips'], $cap['builders'], $cap['forms'] ) as $chip ) { $out .= '' . esc_html( $chip ) . ''; } $out .= '

    '; } if ( $cap['signals'] ) { $out .= '
      '; foreach ( $cap['signals'] as $s ) { $out .= '
    • ' . esc_html( $s['label'] ) . ' ' . esc_html( $s['file'] . ( $s['line'] ? ':' . $s['line'] : '' ) ) . '
    • '; } $out .= '
    '; } if ( '' !== $cap['coverage'] ) { $out .= '

    ' . esc_html( $cap['coverage'] ) . ' These are code patterns, not confirmed features.

    '; } return '
    More about this plugin: ' . esc_html( $cap['label'] ) . '' . $out . '
    '; } /** * Standing limits. * * @param array $limits Standing limits, each with a title and text. */ private static function limits( array $limits ) { $inner = '
      '; foreach ( $limits as $l ) { $inner .= '
    • ' . self::icon( 'unchecked' ) . '' . esc_html( $l['title'] ) . '. ' . esc_html( $l['text'] ) . '
    • '; } $inner .= '

    ShouldCheck reads what a package declares and how its code is written. A passed check means the author said so, or a code pattern was found. It does not mean the plugin works.

    '; return self::group( 'shouldcheck-g-limits', 'info', 'Not covered by ShouldCheck (' . count( $limits ) . ')', $inner, false ); } /** * Warning when the site has changed since a report was made. * * @param array $report Report. * @return string */ public static function drift_notice( array $report ) { if ( empty( $report['environment_hash'] ) ) { return ''; } if ( hash_equals( (string) $report['environment_hash'], ShouldCheck_Site::fingerprint( ShouldCheck_Site::snapshot() ) ) ) { return ''; } return '

    This review is out of date. The site has changed since it was made. Review the plugin again before installing.

    '; } /** * Full saved report page. * * @param array $report Report. * @return string */ public static function report_page( array $report ) { $a = ShouldCheck_Assessment::build( $report ); $m = (array) ( $report['metadata'] ?? array() ); $meta = array( 'Reviewed ' . esc_html( $report['at'] ?? '' ) . ' UTC' . ( ! empty( $report['source'] ) ? ' · ' . esc_html( self::source_label( $report['source'] ) ) : '' ), ); if ( ! empty( $m['file'] ) ) { $meta[] = '' . esc_html( $m['file'] ) . ''; } if ( ! empty( $report['hash'] ) ) { $meta[] = 'SHA-256 ' . esc_html( $report['hash'] ) . ''; } $out = '
    '; $out .= self::drift_notice( $report ); $aside = self::package_card( $report, $meta, $a ); if ( ! empty( $report['installation_result'] ) ) { $aside .= '

    Installation: ' . esc_html( 'completed' === $report['installation_result'] ? 'completed' : 'did not complete' ) . '.

    '; } $out .= '
    ' . self::assessment( $a ) . '
    '; $out .= '
    ' . wp_nonce_field( 'shouldcheck_report_delete', '_wpnonce', true, false ) . '
    '; return $out . '
    '; } /** * Label for a report source. * * @param string $source Source id. * @return string */ public static function source_label( $source ) { $labels = array( 'zip' => 'ZIP upload', 'directory' => 'WordPress.org listing', 'plugin-update' => 'Pending update', ); return $labels[ $source ] ?? 'Review'; } /** * Table of saved reviews, grouped by plugin, version and source. * * @param array $reports Reports, newest first. * @param int $limit Max rows. * @return string */ public static function reports_table( array $reports, $limit = 0 ) { if ( ! $reports ) { return '

    No reviews yet. Check a plugin ZIP, or use the ShouldCheck button on a plugin in Add New.

    '; } $groups = array(); foreach ( $reports as $r ) { $key = strtolower( ( $r['metadata']['name'] ?? '' ) . ':' . ( $r['metadata']['version'] ?? '' ) . ':' . ( $r['source'] ?? 'legacy' ) ); if ( ! isset( $groups[ $key ] ) ) { $groups[ $key ] = array( 'report' => $r, 'count' => 0, ); } ++$groups[ $key ]['count']; } $out = ''; $rows = 0; foreach ( $groups as $g ) { if ( $limit && $rows >= $limit ) { break; } $r = $g['report']; $a = ShouldCheck_Assessment::build( $r ); $labels = array( 'blocked' => 'Blocking problem', 'attention' => 'To verify', 'clear' => 'No problems found', 'neutral' => 'Little could be checked', 'unknown' => 'Not inspected', ); $out .= ''; ++$rows; } return $out . '
    Reviewed (UTC)PluginVersionSourceResultOpen
    ' . esc_html( $r['at'] ?? '' ) . '' . esc_html( $r['metadata']['name'] ?? 'Unknown' ) . ( $g['count'] > 1 ? ' (' . (int) $g['count'] . ' reviews)' : '' ) . '' . esc_html( $r['metadata']['version'] ?? '—' ) . '' . esc_html( self::source_label( $r['source'] ?? '' ) ) . '' . esc_html( $labels[ $a['tone'] ] ?? '' ) . 'Open review of ' . esc_html( $r['metadata']['name'] ?? 'plugin' ) . '
    '; } /** * Human text for timeline event types. * * @param string $type Event type. * @return string */ public static function event_label( $type ) { $labels = array( 'plugin_activated' => 'Plugin activated', 'plugin_deactivated' => 'Plugin deactivated', 'plugin_deleted' => 'Plugin deleted', 'plugin_installed' => 'Plugin installed', 'plugin_removed' => 'Plugin removed', 'plugin_version_changed' => 'Plugin version changed', 'plugin_update_completed' => 'Plugin update completed', 'plugin_activated_observed' => 'Plugin found active', 'plugin_deactivated_observed' => 'Plugin found inactive', 'theme_switched' => 'Theme switched', 'theme_changed' => 'Theme changed', 'environment_changed' => 'Environment changed', 'upgrader_completed' => 'Update completed', 'automatic_updates_completed' => 'Automatic updates ran', ); return $labels[ $type ] ?? ucfirst( str_replace( '_', ' ', (string) $type ) ); } /** * Table of recorded site changes. * * @param array $events Events. * @return string */ public static function timeline_table( array $events ) { if ( ! $events ) { return '

    No changes recorded yet.

    '; } $out = ''; foreach ( $events as $e ) { $d = (array) ( $e['details'] ?? array() ); $detail = isset( $d['component'] ) && is_scalar( $d['component'] ) ? (string) $d['component'] : ( isset( $d['details'] ) && is_scalar( $d['details'] ) ? (string) $d['details'] : '' ); if ( array_key_exists( 'before', $d ) || array_key_exists( 'after', $d ) ) { $fmt = function ( $v ) { return is_scalar( $v ) && '' !== (string) $v ? (string) $v : ( is_array( $v ) && isset( $v['slug'] ) ? $v['slug'] . ' ' . ( $v['version'] ?? '' ) : '—' ); }; $detail .= ' · ' . $fmt( $d['before'] ?? null ) . ' → ' . $fmt( $d['after'] ?? null ); } $out .= ''; } return $out . '
    Observed (UTC)EventDetail
    ' . esc_html( $e['at'] ?? '' ) . '' . esc_html( self::event_label( $e['type'] ?? '' ) ) . '' . esc_html( trim( $detail, " \n\r\t\v\x00" ) ) . '

    ShouldCheck records changes WordPress reports and differences found when it compares the site with its last snapshot. Time is when ShouldCheck noticed, which can be later than the change. It does not record file edits or errors.

    '; } /** * " Your quietest hours are ..." for the money-path line, or ''. * * @return string */ private static function quiet_sentence() { $q = ShouldCheck_Money_Path::quiet_hours(); if ( empty( $q['ok'] ) ) { return ''; } return ' ' . sprintf( 'Your quietest three hours are %1$s to %2$s (%3$s), from %4$d orders over %5$d days.', sprintf( '%02d:00', $q['start'] ), sprintf( '%02d:00', $q['end'] ), wp_timezone_string(), (int) $q['orders'], (int) $q['days'] ); } /** * Yes / no / unknown with a status icon where something needs a look. * * @param bool|null $v Value. * @param string $yes Text for true. * @param string $no Text for false. * @param bool $look Which value needs a look: true, false, or null for none. * @return string */ private static function fact( $v, $yes, $no, $look = null ) { if ( null === $v ) { return 'Not shown in settings'; } $text = $v ? $yes : $no; $icon = ( null !== $look && $v === $look ) ? self::icon( 'verify' ) . ' ' : ''; return $icon . esc_html( $text ); } /** * What the store's checkout looks like from its settings. Nothing here is a test. * * @return string Empty when WooCommerce is not active. */ public static function checkout_card() { $gateways = ShouldCheck_Money_Path::gateways(); if ( null === $gateways ) { return ''; } $site = ShouldCheck_Site::snapshot(); $setup = (array) ( $site['woo_setup'] ?? array() ); $out = '

    Your checkout

    '; $out .= '

    Read from your settings. ShouldCheck did not place an order or take a payment, so none of this shows that checkout works.

    '; $https = 0 === strpos( home_url(), 'https://' ); $rows = array( 'Currency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '—', 'Secure site' => $https ? 'Yes (https)' : 'No (http)', 'Checkout page' => true === ( $setup['checkout_blocks'] ?? null ) ? 'Checkout block' : ( false === ( $setup['checkout_blocks'] ?? null ) ? 'Shortcode or other' : 'Not found' ), ); $out .= '
    '; foreach ( $rows as $k => $v ) { $out .= '
    ' . esc_html( $k ) . '
    ' . ( ( 'Secure site' === $k && ! $https ) ? self::icon( 'verify' ) . ' ' : '' ) . esc_html( $v ) . '
    '; } $out .= '
    '; $enabled = array_values( array_filter( $gateways, function ( $g ) { return ! empty( $g['enabled'] ); } ) ); if ( ! $enabled ) { $out .= '

    No payment method is enabled, so customers cannot pay.

    '; } else { $blocks_page = true === ( $setup['checkout_blocks'] ?? null ); $out .= ''; foreach ( $enabled as $g ) { $plugin = '' !== $g['plugin'] && isset( $site['plugins'][ $g['plugin'] ] ) ? $site['plugins'][ $g['plugin'] ]['name'] : '—'; $out .= ''; } $out .= '
    Payment methodProvided byModeCredentialsCheckout block
    ' . esc_html( $g['title'] ) . '' . esc_html( $plugin ) . '' . self::fact( $g['test'], 'Test mode on', 'Live', true ) . '' . self::fact( $g['keys'], 'Entered', 'Not entered', false ) . '' . ( $blocks_page ? self::fact( $g['blocks'], 'Registered', 'Not registered', false ) : 'Not used on your checkout page' ) . '

    Credentials show only whether a value was entered, never the value. "Not shown in settings" means this plugin keeps the setting somewhere ShouldCheck does not read.

    '; } $q = ShouldCheck_Money_Path::quiet_hours(); if ( ! empty( $q['ok'] ) ) { $out .= '

    Quietest three hours: ' . esc_html( sprintf( '%02d:00 to %02d:00', $q['start'], $q['end'] ) ) . ' (' . esc_html( wp_timezone_string() ) . '). From ' . (int) $q['orders'] . ' paid orders over ' . (int) $q['days'] . ' days. Sales and holidays change this.

    '; } else { $out .= '

    Quiet hours: ' . esc_html( (string) ( $q['reason'] ?? 'not available' ) ) . '

    '; } $open = ShouldCheck_Money_Path::open_checkouts(); $out .= '

    Open checkouts right now: ' . (int) $open . ' started in the last 15 minutes and not yet paid.

    '; return $out . '
    '; } /** * The one entry button ShouldCheck adds to WordPress screens. Always reads "ShouldCheck". * * @param string $href Escaped URL. * @param string $label Accessible name that says what it does. * @param bool $primary Whether it is the main button on the card. * @param string $size Optional size class, such as button-small. * * @return string */ public static function button( $href, $label, $primary = false, $size = '' ) { return 'ShouldCheck'; } /** * A message from WordPress as plain text. WordPress errors often carry HTML, such as a link to the support forums. * Shown escaped, that HTML would print as raw tags. The words are kept and the markup is dropped. * * @param string $message Message that may contain HTML. * @return string */ public static function plain( $message ) { return trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( (string) $message ) ), " \n\r\t\v\x00" ); } /** * One choice shown as a card. The whole card is the radio button. * * @param string $name Field name. * @param string $value Field value. * @param bool $checked Whether it is selected. * @param string $title Title. * @param string $text One-line description. * @param string $preview Small picture of the result (trusted markup built here). * @param string $badge Optional badge text. * @return string */ private static function choice( $name, $value, $checked, $title, $text, $preview, $badge = '' ) { return ''; } /** * Small picture of a button, for the choice cards. * * @param string $label Text. * @param bool $on Whether it is the ShouldCheck button. * @return string */ private static function mini( $label, $on = false ) { return '' . ( $on ? '' : '' ) . esc_html( $label ) . ''; } /** * Settings card. Before the first choice it is a short setup. * * @param array $s Settings. * @return string */ public static function settings_card( array $s ) { $setup = empty( $s['onboarded'] ); $mode = ( $setup || ! empty( $s['beside'] ) ) ? 'beside' : 'manual'; $out = '

    ' . ( $setup ? 'Set up ShouldCheck' : 'Settings' ) . '

    '; $out .= '

    ' . ( $setup ? 'Three choices. Nothing changes until you save, and you can change them later on this screen.' : 'Change a choice and save.' ) . '

    '; $out .= '
    ' . wp_nonce_field( 'shouldcheck_save_settings', '_wpnonce', true, false ); $out .= '
    1. Where should ShouldCheck appear when you add a plugin?
    '; $out .= self::choice( 'mode', 'beside', 'beside' === $mode, 'Next to the install buttons', 'A ShouldCheck button beside Install Now, beside Upload, and on updates. WordPress works as before.', self::mini( 'Install Now' ) . self::mini( 'ShouldCheck', true ), 'Recommended' ); $out .= self::choice( 'mode', 'manual', 'manual' === $mode, 'Only on installed plugins', 'Nothing is added where you install a plugin. To review one first, open Tools, then ShouldCheck.', self::mini( 'ShouldCheck', true ) . self::mini( 'Activate' ) ); $out .= '

    With any choice, ShouldCheck also appears before Activate and Update on plugins you have already installed. It offers a review. It cannot stop an activation made some other way.

    '; $out .= '
    2. May ShouldCheck read plugin code from WordPress.org?
    '; $out .= self::choice( 'package_lookup', 'no', 'yes' !== $s['package_lookup'], 'Listing only', 'Reads declared versions and requirements. No code checks, and nothing is downloaded.', 'Listing · no download' ); $out .= self::choice( 'package_lookup', 'yes', 'yes' === $s['package_lookup'], 'Read the code', 'Downloads the published ZIP, reads it, and deletes it. It is never installed or run.', 'ZIP · read · deleted' ); $out .= '
    '; $keep = ! empty( $s['keep_data'] ); $out .= '
    3. When you delete ShouldCheck, what happens to its data?
    '; $out .= self::choice( 'keep_data', '1', $keep, 'Keep my reviews, history and settings', 'They stay in your database. If you reinstall, ShouldCheck offers the reviews back and remembers your choices.', 'Reviews · history · settings kept', 'Recommended' ); $out .= self::choice( 'keep_data', '0', ! $keep, 'Remove everything', 'Reviews, history and settings are deleted with the plugin.', 'Nothing left behind' ); $out .= '
    '; $out .= '

    '; if ( ! $setup ) { $out .= '
    ' . wp_nonce_field( 'shouldcheck_reset_setup', '_wpnonce', true, false ) . ' Reviews and history stay.
    '; } $out .= '

    Uploaded ZIPs are always read on your site and need no download. What can leave your site: requests to api.wordpress.org for plugin listings, and, if you allow it, to downloads.wordpress.org for the ZIP. ShouldCheck sends nothing to its authors, has no account and runs no tracking. Installs started elsewhere (inside WooCommerce, by FTP or WP-CLI) are not covered.

    '; return $out; } /** * Shown after a reinstall that found data kept from before. * * @param array $kept Counts and date written at uninstall. * @return string */ public static function welcome_back_card( array $kept ) { $when = ! empty( $kept['at'] ) ? gmdate( 'j M Y', (int) strtotime( (string) $kept['at'] ) ) : ''; $r = (int) ( $kept['reviews'] ?? 0 ); $e = (int) ( $kept['events'] ?? 0 ); $set = (bool) ShouldCheck_Settings::get( 'onboarded' ); $out = '

    Welcome back

    '; $out .= '

    ShouldCheck found what it kept when it was removed' . ( '' !== $when ? ' on ' . esc_html( $when ) : '' ) . ': ' . esc_html( (string) $r ) . ' review' . ( 1 === $r ? '' : 's' ) . ', ' . esc_html( (string) $e ) . ' site change' . ( 1 === $e ? '' : 's' ) . '' . ( $set ? ' and your settings' : '' ) . '.

    '; $out .= '
    ' . wp_nonce_field( 'shouldcheck_kept_decide', '_wpnonce', true, false ); $out .= '
    '; $out .= '

    ' . ( $set ? 'Keeping them restores your reviews and history as they were. Your settings are back as you left them, so setup is not asked again. Deleting them starts fresh, settings included.' : 'Keeping them restores your reviews and history as they were. Deleting them starts fresh.' ) . '

    '; return $out; } /** * This-site card. * * @param array $site Snapshot. * @return string */ public static function site_card( array $site ) { $setup = (array) ( $site['woo_setup'] ?? array() ); $yes = function ( $v, $on, $off ) { return true === $v ? $on : ( false === $v ? $off : 'unknown' ); }; $rows = array( 'WordPress' => $site['wordpress'], 'PHP' => $site['php'], 'Database' => $site['db_version'], 'Theme' => $site['theme']['slug'] . ' ' . $site['theme']['version'], 'WooCommerce' => $site['woocommerce'] ? $site['woocommerce'] : 'Not active', ); if ( $site['woocommerce'] ) { $rows['Order storage'] = $yes( $setup['hpos_enabled'] ?? null, 'HPOS tables', 'Legacy (posts)' ); $rows['Checkout page'] = $yes( $setup['checkout_blocks'] ?? null, 'Checkout block', 'Shortcode or other' ); } $active = 0; foreach ( $site['plugins'] as $p ) { if ( $p['active'] ) { ++$active; } } $rows['Plugins'] = $active . ' active of ' . count( $site['plugins'] ) . ' installed'; $out = '
    '; foreach ( $rows as $k => $v ) { $out .= '
    ' . esc_html( $k ) . '
    ' . esc_html( (string) $v ) . '
    '; } return $out . '
    '; } }