Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
- name: Set up ripgrep
run: sudo apt-get update && sudo apt-get install -y ripgrep

- name: Install Composer dependencies
run: composer install --no-interaction --no-progress --prefer-dist

- name: Run PHP syntax checks
run: scripts/php-lint.sh

Expand All @@ -32,3 +35,9 @@ jobs:

- name: Verify public JavaScript assets parse
run: scripts/check-assets.sh

- name: Verify video media rendering
run: php scripts/check-video-support.php

- name: Run PHPStan
run: scripts/phpstan.sh
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/images/
/thumbs/

# Composer dependencies are installed locally and in CI.
/vendor/

# Local utility/debug files.
/audit.php

Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ scripts/php-lint.sh
scripts/check-source-only.sh
scripts/check-file-modes.sh
scripts/check-assets.sh
php scripts/check-video-support.php
scripts/phpstan.sh
```

For browser smoke tests, check:
Expand Down
31 changes: 29 additions & 2 deletions app/includes/conditions.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
$safePageUrl = htmlspecialchars("{$baseUrl}{$_SERVER['REQUEST_URI']}", ENT_QUOTES, 'UTF-8');
$safeTitle = htmlspecialchars($entry['title'] ?? '', ENT_QUOTES, 'UTF-8');
$safeDesc = htmlspecialchars($entry['explanation'] ?? '', ENT_QUOTES, 'UTF-8');
$mediaType = apod_media_type($entry ?? []);
$safeVideoUrl = $mediaType === 'video' ? apod_h(apod_video_embed_url($entry ?? [])) : '';
$dt = $date;
//$safeFullUrl = htmlspecialchars($fullUrl, ENT_QUOTES, 'UTF-8');

Expand All @@ -30,7 +32,27 @@
switch ($page) {
case 'image':

$jsonLd = <<<JSONLD
if ($mediaType === 'video') {
$jsonLd = <<<JSONLD
<script type="application/ld+json">
{
"@context":"https://schema.org",
"@type":"VideoObject",
"name":"{$t}",
"description":"{$d}",
"thumbnailUrl":["{$u}"],
"embedUrl":"{$safeVideoUrl}",
"url":"{$p}",
"uploadDate":"{$dt}",
"datePublished":"{$dt}",
"dateModified":"{$dt}",
"author":{"@type":"Organization","name":"NASA"},
"license":"https://apod.nasa.gov/apod/lib/ApodCopyright.php"
}
</script>
JSONLD;
} else {
$jsonLd = <<<JSONLD
<script type="application/ld+json">
{
"@context":"https://schema.org",
Expand All @@ -46,6 +68,7 @@
}
</script>
JSONLD;
}
$metaTags = <<<HTML
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Expand Down Expand Up @@ -76,7 +99,11 @@
<meta property="og:image:height" content="630">
HTML;
echo $metaTags;
echo '<link rel="stylesheet" href="' . apod_asset_url('assets/css/lightbox.min.css') . '" type="text/css">';
if ($mediaType === 'video') {
echo '<script defer src="' . apod_asset_url('assets/js/video.min.js') . '"></script>';
} else {
echo '<link rel="stylesheet" href="' . apod_asset_url('assets/css/lightbox.min.css') . '" type="text/css">';
}
break;

case 'home':
Expand Down
24 changes: 13 additions & 11 deletions app/includes/lightbox.php
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
<?php
// make sure these are set earlier:
$slug = $entry['slug'];
$titleText = !empty($entry['title']) ? $entry['title'] : 'Image';
$escapedTitle = htmlspecialchars($titleText);
$slug = (string)($entry['slug'] ?? '');
$date = (string)($entry['date'] ?? '');
$basename = "apod-{$date}-full";
$titleText = !empty($entry['title']) ? (string)$entry['title'] : 'Image';
$escapedTitle = apod_h($titleText);

// For your lightbox’s full‐size URL:
$escapedFull = htmlspecialchars($entry['url_full']);
$escapedFull = apod_h($entry['url_full'] ?? '');

// Build a proper srcset from your url_main array
$srcsetParts = [];
$widths = [1200, 980, 640, 440];
foreach ($widths as $w) {
// url_main was generated in your JSON rebuild
$url = $entry['url_main'][$w] ?? $entry['url_thumb'];
$url = $entry['url_main'][$w] ?? ($entry['url_thumb'] ?? '');
if ($url) {
$srcsetParts[] = htmlspecialchars($url) . " {$w}w";
$srcsetParts[] = apod_h((string)$url) . " {$w}w";
}
}
$srcset = implode(",\n ", $srcsetParts);

// Pick a sensible <img> src—here I’m defaulting to the 640px version
$imgSrc = htmlspecialchars($entry['url_main'][640] ?? $entry['url_thumb']);
$imgSrc = apod_h($entry['url_main'][640] ?? ($entry['url_thumb'] ?? ''));
?>

<div class="apod-media">
<a
href="#!"
class="lightbox-trigger"
data-full="<?= $escapedFull ?>"
data-alt="<?= $escapedTitle ?>"
aria-label="View full-size image of <?= $escapedTitle ?>">
<picture>
<source
Expand All @@ -40,7 +42,7 @@ class="lightbox-trigger"
width="1200"
height="675"
decoding="async"
aria-describedby="explanation-<?= htmlspecialchars($slug) ?>"
aria-describedby="explanation-<?= apod_h($slug) ?>"
style="width:100%;height:auto;max-width:1200px;">
</picture>
</a>
Expand All @@ -60,7 +62,7 @@ class="lightbox-trigger"
<p class="visually-hidden">JavaScript is disabled. Displaying standard image.</p>
<img
src="/apod/images/main/980/<?= $basename ?>.webp"
alt="<?= htmlspecialchars($title ?: 'Astronomy Picture of the Day') ?>"
alt="<?= apod_h($titleText) ?>"
width="960"
height="540">
</noscript>
Expand Down Expand Up @@ -130,4 +132,4 @@ function onKeyDown(e) {
backdrop.addEventListener('click', closeLightbox);
})();
})
</script>
</script>
151 changes: 151 additions & 0 deletions app/includes/media.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

declare(strict_types=1);

function apod_h(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
}

/**
* @phpstan-param ApodEntry $entry
*/
function apod_media_type(array $entry): string
{
return strtolower((string)($entry['media_type'] ?? 'image'));
}

/**
* @phpstan-param ApodEntry $entry
*/
function apod_render_media(array $entry): string
{
if (apod_media_type($entry) === 'video') {
return apod_render_video_media($entry);
}

ob_start();
include APOD_APP . '/includes/lightbox.php';
return (string)ob_get_clean();
}

/**
* @phpstan-param ApodEntry $entry
*/
function apod_video_embed_url(array $entry): string
{
$url = (string)($entry['url_video'] ?? $entry['url'] ?? '');
if ($url === '') {
return '';
}

$parts = parse_url($url);
$scheme = strtolower((string)($parts['scheme'] ?? ''));
$host = strtolower((string)($parts['host'] ?? ''));
$allowedHosts = [
'www.youtube.com',
'youtube.com',
'www.youtube-nocookie.com',
'youtube-nocookie.com',
'player.vimeo.com',
'apod.nasa.gov',
];

if ($scheme !== 'https' || !in_array($host, $allowedHosts, true)) {
return '';
}

return $url;
}

/**
* @phpstan-param ApodEntry $entry
*/
function apod_video_poster_url(array $entry): string
{
foreach ([1200, 980, 640, 440] as $width) {
if (!empty($entry['url_main'][$width])) {
return (string)$entry['url_main'][$width];
}
}

if (!empty($entry['url_thumb'])) {
return (string)$entry['url_thumb'];
}

return APOD_BASE_PATH . '/images/placeholder.webp';
}

/**
* @phpstan-param ApodEntry $entry
*/
function apod_render_video_media(array $entry): string
{
$title = (string)($entry['title'] ?? 'Astronomy Picture of the Day video');
$slug = (string)($entry['slug'] ?? preg_replace('/[^a-z0-9-]+/', '-', strtolower($title)));
$posterUrl = apod_video_poster_url($entry);
$embedUrl = apod_video_embed_url($entry);
$frameId = 'apod-video-frame-' . $slug;
$panelId = 'apod-video-panel-' . $slug;

$safeTitle = apod_h($title);
$safePoster = apod_h($posterUrl);
$safeEmbed = apod_h($embedUrl);
$safeFrameId = apod_h($frameId);
$safePanelId = apod_h($panelId);

$button = '';
$frame = '';
$fallback = '';

if ($embedUrl !== '') {
$button = <<<HTML
<button
class="apod-video-play"
type="button"
data-apod-video-load
aria-expanded="false"
aria-controls="{$safePanelId}">
<span aria-hidden="true" class="apod-video-play-icon"></span>
<span>Load video player</span>
</button>
HTML;

$frame = <<<HTML
<div id="{$safePanelId}" class="apod-video-frame" tabindex="-1" hidden>
<iframe
id="{$safeFrameId}"
title="Video: {$safeTitle}"
data-src="{$safeEmbed}"
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
referrerpolicy="strict-origin-when-cross-origin"></iframe>
</div>
HTML;

$fallback = <<<HTML
<noscript>
<p class="apod-video-fallback">
JavaScript is disabled. <a href="{$safeEmbed}" target="_blank" rel="noopener">Open the video for {$safeTitle}</a>.
</p>
</noscript>
HTML;
}

return <<<HTML
<div class="apod-media apod-video" data-apod-video>
<div class="apod-video-poster">
<img
src="{$safePoster}"
alt="{$safeTitle} video poster"
width="1200"
height="675"
decoding="async">
{$button}
</div>

{$frame}
{$fallback}
</div>
HTML;
}
4 changes: 2 additions & 2 deletions app/pages/gallery.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div class="gallery-block">
<div class="gallery-nav">
<div class="view-toggle" role="presentation">
<button aria-label="Toggle List Mode" id="listViewBtn" class="tab-button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="30" height="30"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<button aria-label="Show list view" aria-pressed="false" id="listViewBtn" class="tab-button"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="30" height="30"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path d="M64 144a48 48 0 1 0 0-96 48 48 0 1 0 0 96zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l288 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-288 0zM64 464a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm48-208a48 48 0 1 0 -96 0 48 48 0 1 0 96 0z" />
</svg> <span>List View</span></button>
<button aria-label="Toggle Thumbnail Mode" id="thumbViewBtn" class="tab-button active"><svg width="30" height="30" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<button aria-label="Show gallery view" aria-pressed="true" id="thumbViewBtn" class="tab-button active"><svg width="30" height="30" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path d="M448 80c8.8 0 16 7.2 16 16l0 319.8-5-6.5-136-176c-4.5-5.9-11.6-9.3-19-9.3s-14.4 3.4-19 9.3L202 340.7l-30.5-42.7C167 291.7 159.8 288 152 288s-15 3.7-19.5 10.1l-80 112L48 416.3l0-.3L48 96c0-8.8 7.2-16 16-16l384 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm80 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z" />
</svg><span>Gallery View</span></button>
</div>
Expand Down
Loading