response) && is_array($updates->response) ? $updates->response : []; foreach ($plugins as $file => $meta) { $slug = dirname($file); if ($slug === '.' || $slug === '') $slug = basename($file, '.php'); $info = $this->wp_org_info($slug); $detail_parts = []; $level = 'ok'; if ($info === 'not_in_repo') { $level = 'warn'; $detail_parts[] = 'Not in WP.org repo (could be premium, custom, or removed)'; $removed++; } elseif ($info === 'removed') { $level = 'bad'; $detail_parts[] = '⚠ Removed from WP.org repo — possible abandonment or compromise'; $removed++; } elseif (is_array($info)) { if (!empty($info['last_updated'])) { $age_days = floor((time() - strtotime($info['last_updated'])) / DAY_IN_SECONDS); $detail_parts[] = 'Last release ' . $age_days . ' day(s) ago (' . $info['last_updated'] . ')'; if ($age_days > 365) { $level = 'warn'; $abandoned++; $detail_parts[] = 'no release in >12 months'; } } if (!empty($info['active_installs'])) { $detail_parts[] = number_format($info['active_installs']) . ' active installs'; } } if (isset($update_map[$file])) { $level = 'warn'; $detail_parts[] = 'Update available → ' . ($update_map[$file]->new_version ?? '?'); $update_avail++; } $f[] = $this->finding( 'plugin_' . sanitize_key($file), $level, $meta['Name'] ?? $file, (string) ($meta['Version'] ?? ''), implode(' · ', $detail_parts) ); } // Summary at the top array_unshift($f, $this->finding( 'summary', 'info', 'Summary', count($plugins) . ' plugins', $update_avail . ' with updates · ' . $abandoned . ' not updated in 12 mo · ' . $removed . ' not in WP.org' )); return $f; } /** * Returns ['name' => ..., 'last_updated' => ..., 'active_installs' => ...] * or 'not_in_repo' / 'removed' on failure. Cached 24h per slug via transient. */ private function wp_org_info(string $slug) { $cache_key = 'wph_pi_' . md5($slug); $cached = get_transient($cache_key); if ($cached !== false) return $cached; $url = 'https://api.wordpress.org/plugins/info/1.0/' . rawurlencode($slug) . '.json'; $resp = wp_remote_get($url, ['timeout' => 4]); if (is_wp_error($resp)) { set_transient($cache_key, 'not_in_repo', HOUR_IN_SECONDS); return 'not_in_repo'; } $code = wp_remote_retrieve_response_code($resp); $body = wp_remote_retrieve_body($resp); if ($code === 404) { set_transient($cache_key, 'not_in_repo', DAY_IN_SECONDS); return 'not_in_repo'; } $data = json_decode((string) $body, true); if (!is_array($data)) { set_transient($cache_key, 'not_in_repo', HOUR_IN_SECONDS); return 'not_in_repo'; } if (isset($data['error'])) { set_transient($cache_key, 'removed', DAY_IN_SECONDS); return 'removed'; } $out = [ 'name' => $data['name'] ?? $slug, 'last_updated' => $data['last_updated'] ?? null, 'active_installs' => $data['active_installs'] ?? null, ]; set_transient($cache_key, $out, DAY_IN_SECONDS); return $out; } };