| Server IP : 3.147.158.171 / Your IP : 216.73.216.216 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/repo/sites/scike-astronomy/wp-content/plugins/tsai-plugin/includes/ |
Upload File : |
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* [tsai_projects] — renders every "promoted" site (logo + name + tagline)
* together with a short list of each site's recent articles.
*
* Site data comes from the TSAI API /site-index endpoint (the same feed the
* Banner app uses). Recent articles are pulled per-site from each site's public
* WordPress REST API.
*/
class TSAI_Projects {
const ARTICLES_TTL = 30 * MINUTE_IN_SECONDS;
const ARTICLES_MISS_TTL = 5 * MINUTE_IN_SECONDS;
public function __construct() {
add_shortcode('tsai_projects', array($this, 'render'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_assets'));
}
public function render($atts) {
$atts = shortcode_atts(array(
'count' => '5', // recent articles per site
'logo' => '150', // logo size: 72 | 150 | 300 | full
'exclude_self' => '0', // '1' to drop the current site from the list
'remote' => '1', // '1' to also include sites from remote servers
), $atts, 'tsai_projects');
$count = max(1, (int) $atts['count']);
$exclude_self = filter_var($atts['exclude_self'], FILTER_VALIDATE_BOOLEAN);
$include_remote = filter_var($atts['remote'], FILTER_VALIDATE_BOOLEAN);
$sites = $include_remote
? TSAI_Site_Index::get_all_sites()
: TSAI_Site_Index::get_sites();
if (empty($sites)) {
return sprintf(
'<div class="tsai-projects tsai-projects--empty"><p>%s</p></div>',
esc_html__('No projects to show right now.', 'tsai')
);
}
$home_host = wp_parse_url(home_url(), PHP_URL_HOST);
$cards = '';
foreach ($sites as $site) {
if (!is_array($site) || empty($site['promoted'])) {
continue;
}
if ($exclude_self) {
$entry_host = wp_parse_url(isset($site['wpHost']) ? $site['wpHost'] : '', PHP_URL_HOST);
if ($entry_host && $home_host && strcasecmp($entry_host, $home_host) === 0) {
continue;
}
}
$cards .= $this->render_card($site, $count, $atts['logo']);
}
if ($cards === '') {
return sprintf(
'<div class="tsai-projects tsai-projects--empty"><p>%s</p></div>',
esc_html__('No projects to show right now.', 'tsai')
);
}
return sprintf('<div class="tsai-projects">%s</div>', $cards);
}
private function render_card($site, $count, $logo_size) {
$name = isset($site['name']) ? (string) $site['name'] : '';
$tagline = isset($site['tagline']) ? (string) $site['tagline'] : '';
$link = isset($site['link']) ? (string) $site['link'] : '';
$wp_host = isset($site['wpHost']) ? (string) $site['wpHost'] : '';
$logo = $this->pick_logo($site, $logo_size);
// Header: logo (linked) + name (linked) + tagline.
$logo_html = '';
if ($logo !== '') {
$img = sprintf(
'<img class="tsai-projects__logo" src="%s" alt="%s" loading="lazy" />',
esc_url($logo),
esc_attr($name)
);
$logo_html = $link !== ''
? sprintf('<a class="tsai-projects__logo-link" href="%s">%s</a>', esc_url($link), $img)
: $img;
}
$name_html = esc_html($name);
if ($link !== '') {
$name_html = sprintf('<a href="%s">%s</a>', esc_url($link), $name_html);
}
// Two sites on different servers can share a slug and a display name
// (the tsai server and scike.ai both host a "News"), so print the host
// to say which one this card leads to.
$host = $this->display_host($wp_host);
$host_html = $host !== ''
? sprintf('<p class="tsai-projects__host">%s</p>', esc_html($host))
: '';
$tagline_html = $tagline !== ''
? sprintf('<p class="tsai-projects__tagline">%s</p>', esc_html($tagline))
: '';
$articles_html = $this->articles_html($wp_host, $count);
return sprintf(
'<div class="tsai-projects__card">'
. '<div class="tsai-projects__header">%s<h3 class="tsai-projects__title">%s</h3>%s%s</div>'
. '%s'
. '</div>',
$logo_html,
$name_html,
$host_html,
$tagline_html,
$articles_html
);
}
/**
* The host as shown to a visitor, minus "www.".
*/
private function display_host($wp_host) {
$wp_host = trim((string) $wp_host);
if ($wp_host === '') {
return '';
}
$host = wp_parse_url($wp_host, PHP_URL_HOST);
$host = strtolower($host ? $host : $wp_host);
return strpos($host, 'www.') === 0 ? substr($host, 4) : $host;
}
private function pick_logo($site, $logo_size) {
$map = array(
'72' => 'logo72',
'150' => 'logo150',
'300' => 'logo300',
'full' => 'logoFull',
);
$key = isset($map[$logo_size]) ? $map[$logo_size] : 'logo150';
// Chosen size, then sensible fallbacks.
foreach (array($key, 'logo150', 'logo300', 'logoFull', 'logo72') as $k) {
if (!empty($site[$k])) {
return (string) $site[$k];
}
}
return '';
}
private function articles_html($wp_host, $count) {
$articles = $this->get_recent_articles($wp_host, $count);
if (empty($articles)) {
return sprintf(
'<p class="tsai-projects__empty">%s</p>',
esc_html__('No recent articles.', 'tsai')
);
}
$items = '';
foreach ($articles as $a) {
$meta_parts = array();
if (!empty($a['author'])) {
$meta_parts[] = esc_html($a['author']);
}
if (!empty($a['date'])) {
$ts = strtotime($a['date']);
if ($ts) {
$meta_parts[] = esc_html(date_i18n(get_option('date_format'), $ts));
}
}
$meta_html = !empty($meta_parts)
? sprintf('<span class="tsai-projects__article-meta">%s</span>', implode(' · ', $meta_parts))
: '';
$items .= sprintf(
'<li class="tsai-projects__article"><a class="tsai-projects__article-title" href="%s">%s</a>%s</li>',
esc_url($a['link']),
esc_html($a['title']),
$meta_html
);
}
return sprintf('<ul class="tsai-projects__articles">%s</ul>', $items);
}
/**
* Fetch recent published articles from a site's public WP REST API.
* Cached per host+count in a transient.
*
* @return array List of ['title','link','date','author'] rows.
*/
private function get_recent_articles($wp_host, $count) {
$wp_host = trim((string) $wp_host);
if ($wp_host === '') {
return array();
}
$cache_key = 'tsai_projects_articles_' . md5($wp_host . '|' . $count);
$cached = get_transient($cache_key);
if (is_array($cached)) {
return $cached;
}
$endpoint = trailingslashit($wp_host) . 'wp-json/wp/v2/posts';
$endpoint = add_query_arg(array(
'_embed' => 'author,wp:featuredmedia',
'per_page' => $count,
'status' => 'publish',
'orderby' => 'date',
'order' => 'desc',
), $endpoint);
$response = wp_remote_get($endpoint, array('timeout' => 8));
if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
error_log('[TSAI] projects: recent articles fetch failed for ' . $wp_host);
set_transient($cache_key, array(), self::ARTICLES_MISS_TTL);
return array();
}
$posts = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($posts)) {
set_transient($cache_key, array(), self::ARTICLES_MISS_TTL);
return array();
}
$articles = array();
foreach ($posts as $post) {
if (!is_array($post)) {
continue;
}
$articles[] = array(
'title' => isset($post['title']['rendered']) ? wp_strip_all_tags($post['title']['rendered']) : '',
'link' => isset($post['link']) ? (string) $post['link'] : '',
'date' => isset($post['date']) ? (string) $post['date'] : '',
'author' => $this->extract_author($post),
);
}
set_transient($cache_key, $articles, self::ARTICLES_TTL);
return $articles;
}
private function extract_author($post) {
if (isset($post['_embedded']['author'][0]['name'])) {
return (string) $post['_embedded']['author'][0]['name'];
}
return '';
}
public function enqueue_assets() {
// Enqueue unconditionally: block themes leave post_content empty, so
// has_shortcode() misses shortcodes rendered via patterns/templates.
wp_enqueue_style('tsai-projects', TSAI_PLUGIN_URL . 'assets/css/tsai-projects.css', array(), '2.4.3');
}
}
new TSAI_Projects();