Plugin Directory

Changeset 3780742


Ignore:
Timestamp:
11/02/2026 09:12:28 PM (32 hours ago)
Author:
omniswp
Message:

siteward 0.17.1

Location:
siteward
Files:
36 added
11 edited

Legend:

Unmodified
Added
Removed
  • siteward/trunk/admin/assets/admin.css

    r3757358 r3780742  
    170170.omnis-kv .v.warn { color: #b54708; }
    171171.omnis-kv .v.bad { color: #b42318; }
     172.omnis-kv .v .warn { color: #b54708; }
     173.omnis-kv .v .bad { color: #b42318; }
    172174.omnis-kv .v.ok { color: #067647; }
    173175.omnis-inline-warn { margin-top: 11px; background: #fffaeb; border: 1px solid #fedf89; border-radius: 8px; padding: 8px 10px; font-size: 12px; color: #b54708; }
  • siteward/trunk/admin/class-omnis-admin.php

    r3757358 r3780742  
    400400                        'notify_vulnerable'    => empty( $in['notify_vulnerable'] ) ? 0 : 1,
    401401                        'notify_rollback'      => empty( $in['notify_rollback'] ) ? 0 : 1,
     402                        'vuln_notices'         => empty( $in['vuln_notices'] ) ? 0 : 1,
    402403                        // 1 to 168 hours: below an hour the feed client's own floor rejects it
    403404                        // anyway, and beyond a week a "current" feed is not current.
  • siteward/trunk/admin/views/settings.php

    r3757358 r3780742  
    147147                                <p class="section-desc"><?php esc_html_e( 'How often this dashboard fetches the vulnerability data it matches your sites against.', 'siteward' ); ?></p>
    148148                                <?php omnis_num_row( 'vuln_interval', __( 'Check every (hours)', 'siteward' ), $settings, '', 1 ); ?>
     149                                <div class="omnis-row" style="border-top:1px solid #f4f4f5;margin-top:10px">
     150                                        <label><input type="checkbox" name="vuln_notices" value="1" <?php checked( ! empty( $settings['vuln_notices'] ) ); ?> /> <?php esc_html_e( 'Warn on each site\'s Plugins screen', 'siteward' ); ?>
     151                                                <span class="field-desc"><?php esc_html_e( 'Adds a row under each affected plugin on the site itself, seen only by people who can update plugins there, with the version that fixes it. Sites need Siteward Child 0.9.0 or later. Turn it off if clients log in and should not see these warnings.', 'siteward' ); ?></span>
     152                                        </label>
     153                                </div>
    149154                                <p class="section-desc" style="margin:12px 0 0">
    150155                                        <?php
  • siteward/trunk/admin/views/site-detail.php

    r3747027 r3780742  
    168168                <div class="omnis-info-card">
    169169                        <h3>⛭ <?php esc_html_e( 'Environment', 'siteward' ); ?></h3>
    170                         <div class="omnis-kv"><span class="k"><?php esc_html_e( 'WordPress', 'siteward' ); ?></span><span class="v"><?php echo esc_html( $status && $status->wp_version ? $status->wp_version : '—' ); ?><?php echo ! empty( $core['available'] ) ? ' <span class="warn">· ' . esc_html__( 'update ready', 'siteward' ) . '</span>' : ''; ?></span></div>
    171                         <div class="omnis-kv"><span class="k"><?php esc_html_e( 'PHP', 'siteward' ); ?></span><span class="v"><?php echo esc_html( $status && $status->php_version ? $status->php_version : '—' ); ?></span></div>
     170                        <?php
     171                        $omnis_wp_behind = $status && $status->wp_version ? Omnis_Flags::wp_releases_behind( $status->wp_version, Omnis_Flags::latest_wp( isset( $core['latest'] ) ? (string) $core['latest'] : '' ) ) : 0;
     172                        $omnis_php_eol   = $status && $status->php_version ? Omnis_Flags::php_eol( $status->php_version ) : null;
     173                        ?>
     174                        <div class="omnis-kv"><span class="k"><?php esc_html_e( 'WordPress', 'siteward' ); ?></span><span class="v"><?php echo esc_html( $status && $status->wp_version ? $status->wp_version : '—' ); ?><?php echo ! empty( $core['available'] ) ? ' <span class="warn">· ' . esc_html__( 'update ready', 'siteward' ) . '</span>' : ''; ?><?php if ( $omnis_wp_behind >= 2 ) : ?> <span class="bad">· <?php echo esc_html( sprintf( /* translators: %d: number of major WordPress releases */ _n( '%d release behind', '%d releases behind', $omnis_wp_behind, 'siteward' ), $omnis_wp_behind ) ); ?></span><?php endif; ?></span></div>
     175                        <div class="omnis-kv"><span class="k"><?php esc_html_e( 'PHP', 'siteward' ); ?></span><span class="v"><?php echo esc_html( $status && $status->php_version ? $status->php_version : '—' ); ?>
     176                                <?php if ( $omnis_php_eol && $omnis_php_eol['ended'] ) : ?>
     177                                        <span class="bad" title="<?php esc_attr_e( 'This PHP version no longer gets security fixes. Ask the host to move the site to a supported version.', 'siteward' ); ?>">· <?php echo '' !== $omnis_php_eol['date'] ? esc_html( sprintf( /* translators: %s: date */ __( 'unsupported since %s', 'siteward' ), date_i18n( get_option( 'date_format' ), strtotime( $omnis_php_eol['date'] ) ) ) ) : esc_html__( 'unsupported', 'siteward' ); ?></span>
     178                                <?php elseif ( $omnis_php_eol && $omnis_php_eol['days_left'] <= Omnis_Flags::PHP_EOL_SOON_DAYS ) : ?>
     179                                        <span class="warn" title="<?php esc_attr_e( 'After this date this PHP version gets no more security fixes.', 'siteward' ); ?>">· <?php echo esc_html( sprintf( /* translators: %s: date */ __( 'security support ends %s', 'siteward' ), date_i18n( get_option( 'date_format' ), strtotime( $omnis_php_eol['date'] ) ) ) ); ?></span>
     180                                <?php endif; ?>
     181                        </span></div>
    172182                        <?php if ( $status && ! empty( $status->server_ip ) ) : ?>
    173183                                <?php $omnis_ip_public = filter_var( $status->server_ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); ?>
  • siteward/trunk/includes/class-omnis-flags.php

    r3708286 r3780742  
    2727        const SITE_HEALTH       = 'site_health';
    2828        const VULNERABLE        = 'vulnerable';
     29        const OUTDATED_PHP      = 'outdated_php';
     30        const OUTDATED_WP       = 'outdated_wp';
     31
     32        /**
     33         * End of security support for each PHP branch, from php.net/supported-versions.
     34         * A branch older than the first entry ended long ago; a branch newer than the
     35         * last is not judged until it is added here, so add each new branch when it
     36         * ships (8.6 / 9.0 expected late 2026, supported until the end of 2030).
     37         */
     38        const PHP_EOL = array(
     39                '7.4' => '2022-11-28',
     40                '8.0' => '2023-11-26',
     41                '8.1' => '2025-12-31',
     42                '8.2' => '2026-12-31',
     43                '8.3' => '2027-12-31',
     44                '8.4' => '2028-12-31',
     45                '8.5' => '2029-12-31',
     46        );
     47
     48        /** How soon before a PHP branch's end the site page starts warning, in days. */
     49        const PHP_EOL_SOON_DAYS = 180;
    2950
    3051        /**
    … …  
    4869                        self::FAILING_CRON      => array( 'label' => __( 'Cron failing', 'siteward' ), 'severity' => 'warning' ),
    4970                        self::SITE_HEALTH       => array( 'label' => __( 'Site Health issues', 'siteward' ), 'severity' => 'warning' ),
     71                        self::OUTDATED_PHP      => array( 'label' => __( 'Unsupported PHP', 'siteward' ), 'severity' => 'warning' ),
     72                        self::OUTDATED_WP       => array( 'label' => __( 'Outdated WordPress', 'siteward' ), 'severity' => 'warning' ),
    5073                        self::UPDATES_AVAILABLE => array( 'label' => __( 'Updates available', 'siteward' ), 'severity' => 'info' ),
    5174                );
    … …  
    178201                        if ( ! empty( $health['site_health']['critical'] ) ) {
    179202                                $flags[] = self::SITE_HEALTH;
     203                        }
     204                }
     205
     206                // PHP past the end of its security support, and WordPress two or more
     207                // major releases behind. Both versions come from the site's last sync.
     208                if ( ! empty( $snapshot['php_version'] ) ) {
     209                        $eol = self::php_eol( (string) $snapshot['php_version'] );
     210                        if ( $eol && $eol['ended'] ) {
     211                                $flags[] = self::OUTDATED_PHP;
     212                        }
     213                }
     214                if ( ! empty( $snapshot['wp_version'] ) ) {
     215                        $latest = self::latest_wp( isset( $updates['core']['latest'] ) ? (string) $updates['core']['latest'] : '' );
     216                        if ( self::wp_releases_behind( (string) $snapshot['wp_version'], $latest ) >= 2 ) {
     217                                $flags[] = self::OUTDATED_WP;
    180218                        }
    181219                }
    … …  
    220258                        self::VULNERABLE        => array( 'label' => __( 'Known vulnerability', 'siteward' ), 'flags' => array( self::VULNERABLE ) ),
    221259                        self::SLOW_LOAD         => array( 'label' => __( 'Slow', 'siteward' ), 'flags' => array( self::SLOW_LOAD ) ),
     260                        self::OUTDATED_PHP      => array( 'label' => __( 'Unsupported PHP', 'siteward' ), 'flags' => array( self::OUTDATED_PHP ) ),
     261                        self::OUTDATED_WP       => array( 'label' => __( 'Outdated WordPress', 'siteward' ), 'flags' => array( self::OUTDATED_WP ) ),
    222262                        'healthy'               => array( 'label' => __( 'Healthy', 'siteward' ), 'special' => 'healthy' ),
    223263                );
    … …  
    261301                }
    262302                return $keys;
     303        }
     304
     305        /**
     306         * Where a PHP version stands against its branch's end of security support.
     307         *
     308         * @param string $version e.g. "8.1.33".
     309         * @return array|null { branch, date (Y-m-d, '' if before the table), ended, days_left },
     310         *                    or null when the version cannot be read or is newer than the table.
     311         */
     312        public static function php_eol( $version ) {
     313                if ( ! preg_match( '/^(\d+)\.(\d+)/', (string) $version, $m ) ) {
     314                        return null;
     315                }
     316                $branch = $m[1] . '.' . $m[2];
     317                $first  = array_key_first( self::PHP_EOL );
     318                if ( version_compare( $branch, $first, '<' ) ) {
     319                        return array( 'branch' => $branch, 'date' => '', 'ended' => true, 'days_left' => 0 );
     320                }
     321                if ( ! isset( self::PHP_EOL[ $branch ] ) ) {
     322                        return null;
     323                }
     324                $date = self::PHP_EOL[ $branch ];
     325                $end  = strtotime( $date . ' 23:59:59 UTC' );
     326                return array(
     327                        'branch'    => $branch,
     328                        'date'      => $date,
     329                        'ended'     => time() > $end,
     330                        'days_left' => max( 0, (int) floor( ( $end - time() ) / DAY_IN_SECONDS ) ),
     331                );
     332        }
     333
     334        /**
     335         * The latest WordPress release, as the highest of: the dashboard's own core
     336         * update check, the dashboard's own WordPress, the newest version any managed
     337         * site runs, and what this site reported. No single source is enough: the
     338         * update check can come back empty, and a site on old PHP is only offered the
     339         * newest release its PHP can run, which would hide exactly the sites that matter.
     340         *
     341         * @param string $reported The latest version the site reported, if any.
     342         * @return string Version, or '' when unknown.
     343         */
     344        public static function latest_wp( $reported = '' ) {
     345                static $known = null;
     346                if ( null === $known ) {
     347                        global $wpdb;
     348                        $candidates = array( (string) get_bloginfo( 'version' ) );
     349                        $core       = get_site_transient( 'update_core' );
     350                        if ( is_object( $core ) && ! empty( $core->offers ) && is_array( $core->offers ) ) {
     351                                foreach ( $core->offers as $offer ) {
     352                                        if ( is_object( $offer ) && ! empty( $offer->current ) ) {
     353                                                $candidates[] = (string) $offer->current;
     354                                        }
     355                                }
     356                        }
     357                        $table = Omnis_DB::table( Omnis_DB::STATUS );
     358                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table name, once per request (static).
     359                        $fleet = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT wp_version FROM {$table} WHERE wp_version <> %s", '' ) );
     360                        $known = self::max_version( array_merge( $candidates, (array) $fleet ) );
     361                }
     362                return self::max_version( array( $known, (string) $reported ) );
     363        }
     364
     365        /**
     366         * The highest of a list of version strings ('' when none is valid). Releases
     367         * only: one test site on a beta or RC must not make every site look behind.
     368         */
     369        private static function max_version( array $versions ) {
     370                $max = '';
     371                foreach ( $versions as $v ) {
     372                        if ( preg_match( '/^\d+\.\d+(\.\d+)?$/', (string) $v ) && ( '' === $max || version_compare( $v, $max, '>' ) ) ) {
     373                                $max = (string) $v;
     374                        }
     375                }
     376                return $max;
     377        }
     378
     379        /**
     380         * How many major WordPress releases (6.9, 7.0, 7.1, ...) a version is behind.
     381         *
     382         * @param string $version Site's version.
     383         * @param string $latest  Latest version.
     384         * @return int 0 when current, ahead or unknown.
     385         */
     386        public static function wp_releases_behind( $version, $latest ) {
     387                if ( ! preg_match( '/^(\d+)\.(\d+)/', (string) $version, $a ) || ! preg_match( '/^(\d+)\.(\d+)/', (string) $latest, $b ) ) {
     388                        return 0;
     389                }
     390                // WordPress majors run x.0 to x.9, so x * 10 + y counts releases.
     391                return max( 0, ( (int) $b[1] * 10 + (int) $b[2] ) - ( (int) $a[1] * 10 + (int) $a[2] ) );
    263392        }
    264393
    … …  
    311440                                'health'         => ! empty( $row->health ) ? json_decode( $row->health, true ) : array(),
    312441                                'vulns'          => ! empty( $row->vulns ) ? json_decode( $row->vulns, true ) : array(),
     442                                'php_version'    => (string) $row->php_version,
     443                                'wp_version'     => (string) $row->wp_version,
    313444                                'load_time_ms'   => (int) $row->load_time_ms,
    314445                                'is_up'          => $site && null !== $site->is_up ? (bool) $site->is_up : null,
  • siteward/trunk/includes/class-omnis-settings.php

    r3757358 r3780742  
    4040                        'notify_vulnerable'    => 1,      // alert on newly discovered vulnerabilities
    4141                        'notify_rollback'      => 1,      // alert when an update was rolled back
     42                        'vuln_notices'         => 1,      // show known vulnerabilities on each site's Plugins screen (child 0.9.0+)
    4243                        'vuln_interval'        => 12,     // hours between vulnerability feed checks
    4344                );
  • siteward/trunk/includes/class-omnis-sync.php

    r3708286 r3780742  
    4545                foreach ( $sites as $s ) {
    4646                        $by_id[ (int) $s->id ] = $s;
    47                         $spec                  = Omnis_Connection::build_request( $s, 'get_status' );
     47                        $spec                  = Omnis_Connection::build_request( $s, 'get_status', self::status_params( (int) $s->id ) );
    4848                        if ( is_wp_error( $spec ) ) {
    4949                                $errored[ (int) $s->id ] = $spec;
    … …  
    7070
    7171        /**
     72         * What a status pull hands the site: its known vulnerabilities, for the
     73         * warning rows on its own Plugins screen (child 0.9.0+; older children
     74         * ignore it). An empty list with on=false tells the child to clear them.
     75         *
     76         * @param int $site_id Site ID.
     77         * @return array
     78         */
     79        private static function status_params( $site_id ) {
     80                $on = (bool) Omnis_Settings::get( 'vuln_notices', 1 ) && class_exists( 'Omnis_Vuln' );
     81                return array(
     82                        'vuln_notices' => array(
     83                                'on'    => $on,
     84                                'items' => $on ? Omnis_Vuln::notices_for_site( $site_id ) : array(),
     85                        ),
     86                );
     87        }
     88
     89        /**
    7290         * Sync a single site (on-demand path).
    7391         *
    … …  
    8199                }
    82100
    83                 $spec = Omnis_Connection::build_request( $site, 'get_status' );
     101                $spec = Omnis_Connection::build_request( $site, 'get_status', self::status_params( (int) $site_id ) );
    84102                if ( is_wp_error( $spec ) ) {
    85103                        self::process_result( $site, $spec );
  • siteward/trunk/includes/class-omnis-uptime.php

    r3757358 r3780742  
    339339                        // otherwise be dropped from the recomputed flags.
    340340                        'vulns'          => $row && ! empty( $row->vulns ) ? json_decode( $row->vulns, true ) : array(),
     341                        'php_version'    => $row ? (string) $row->php_version : '',
     342                        'wp_version'     => $row ? (string) $row->wp_version : '',
    341343                        'load_time_ms'   => $is_up ? (int) $resp_ms : ( $row ? (int) $row->load_time_ms : 0 ),
    342344                        'is_up'          => $is_up,
  • siteward/trunk/includes/class-omnis-vuln.php

    r3655630 r3780742  
    467467
    468468        /**
     469         * What a site's own Plugins screen should warn about: one short entry per
     470         * affected plugin, sent to the child with each status sync. Informational
     471         * advisories are left out, as they are from the flag. The child shows an
     472         * entry only while the plugin is still on the version scanned here.
     473         *
     474         * @param int $site_id Site ID.
     475         * @return array List of { file, version, severity, count, fix, unfixable, cve, title, link }.
     476         */
     477        public static function notices_for_site( $site_id ) {
     478                $real = array_filter(
     479                        self::for_site( $site_id ),
     480                        function ( $f ) {
     481                                return 'plugin' === ( $f['y'] ?? '' ) && '' !== (string) ( $f['f'] ?? '' ) && empty( $f['e']['n'] );
     482                        }
     483                );
     484                $out = array();
     485                foreach ( self::group( $real ) as $g ) {
     486                        // The advisory to name: the worst one, preferring one with a CVE.
     487                        $rank = array_flip( self::SEVERITIES );
     488                        $pick = null;
     489                        $best = PHP_INT_MAX;
     490                        foreach ( $g['entries'] as $e ) {
     491                                $r = isset( $rank[ self::severity( $e ) ] ) ? $rank[ self::severity( $e ) ] : 99;
     492                                if ( null === $pick || $r < $best || ( $r === $best && empty( $pick['c'] ) && ! empty( $e['c'] ) ) ) {
     493                                        $pick = $e;
     494                                        $best = $r;
     495                                }
     496                        }
     497                        $out[] = array(
     498                                'file'      => (string) $g['file'],
     499                                'version'   => (string) $g['version'],
     500                                'severity'  => (string) $g['worst'],
     501                                'count'     => (int) $g['count'],
     502                                'fix'       => (string) $g['fix'],
     503                                'unfixable' => ! empty( $g['unfixable'] ),
     504                                'cve'       => isset( $pick['c'] ) ? (string) $pick['c'] : '',
     505                                'title'     => isset( $pick['t'] ) ? (string) $pick['t'] : '',
     506                                'link'      => $pick ? Omnis_Vuln_Feed::link( $pick ) : '',
     507                        );
     508                }
     509                return $out;
     510        }
     511
     512        /**
    469513         * A site's findings, already grouped by software (for the site detail page).
    470514         *
  • siteward/trunk/readme.txt

    r3769325 r3780742  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.16.2
     7Stable tag: 0.17.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8787
    8888== Changelog ==
     89
     90= 0.17.1 =
     91* New: known-vulnerability warnings on each site's own Plugins screen. With each sync the dashboard hands a site its matches, and a row appears under each affected plugin with its rating and the version that fixes it, for people who can update plugins there. On by default; switch it off under Settings, Vulnerability feed (for example if clients log in). Needs Siteward Child 0.9.0 or later on the site.
     92* New: Unsupported PHP flag. A site running a PHP version that no longer gets security fixes (8.1 and older today) is flagged, with its own filter on the Sites page. The end-of-support dates come from php.net and are kept in one list in the plugin, so 8.2 is flagged automatically once its support ends on 31 December 2026.
     93* New: the site page shows when a site's PHP version stops getting security fixes, from six months before the date, and how long ago it stopped for older versions.
     94* New: Outdated WordPress flag, for sites two or more major releases behind the latest WordPress, with its own filter. The site page shows how many releases behind it is.
    8995
    9096= 0.16.2 =
  • siteward/trunk/siteward.php

    r3769325 r3780742  
    44 * Plugin URI:        https://omniswp.com/features/
    55 * Description:        Monitor and update many WordPress sites from one lightweight, self-hosted dashboard: uptime monitoring with email alerts, one-click and bulk core/plugin/theme updates, and critical-error detection.
    6  * Version:           0.16.2
     6 * Version:           0.17.1
    77 * Requires at least: 5.8
    88 * Tested up to:     7.1
    … …  
    2020defined( 'ABSPATH' ) || exit;
    2121
    22 define( 'OMNIS_VERSION', '0.16.2' );
     22define( 'OMNIS_VERSION', '0.17.1' );
    2323define( 'OMNIS_DB_VERSION', '10' );
    2424define( 'OMNIS_FILE', __FILE__ );
Note: See TracChangeset for help on using the changeset viewer.