Skip to content
Open
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
98 changes: 73 additions & 25 deletions api/README.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions api/docker/DockerfilePrivateDemo
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This Dockerfile builds a public frontend for the private STACK API demo.
# It serves the browser UI and privately calls an API container over the
# Docker network. It does not expose the STACK API routes directly.

FROM composer:lts AS composer

COPY / /work/

WORKDIR /work/api/

RUN composer install
RUN composer dump-autoload --optimize

FROM php:8.4.5-apache-bookworm

RUN a2enmod rewrite

ENV APACHE_DOCUMENT_ROOT=/srv/stack/api/private-demo/public
ENV STACK_PRIVATE_DEMO_API_URL=http://api
ENV STACK_PRIVATE_DEMO_LIBRARY=stacklibrary

RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

COPY ./api/docker/files/private-demo/000-default.conf /etc/apache2/sites-available/000-default.conf
COPY / /srv/stack
COPY --from=composer /work/api/vendor /srv/stack/api/vendor

RUN mkdir -p /srv/stack/api/private-demo/public/assets && \
cp /srv/stack/api/public/api.css /srv/stack/api/private-demo/public/assets/api.css && \
cp /srv/stack/api/public/stackjsvle.js /srv/stack/api/private-demo/public/assets/stackjsvle.js && \
cp /srv/stack/api/public/stackshared.js /srv/stack/api/private-demo/public/assets/stackshared.js && \
cp /srv/stack/api/public/logo_large.png /srv/stack/api/private-demo/public/assets/logo_large.png && \
cp /srv/stack/api/public/favicon.ico /srv/stack/api/private-demo/public/favicon.ico
58 changes: 58 additions & 0 deletions api/docker/docker-compose.demo-private.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Development version of the private STACK API demo.
#
# Only the frontend is exposed to the host. Source is bind-mounted so PHP,
# JavaScript, CSS, samplequestions, and API changes are visible without rebuilding.
services:
maxima:
image: mathinstitut/goemaxima:2026062900-latest
tmpfs:
- "/tmp"
restart: unless-stopped
cap_add:
- SETGID
- SETUID
cap_drop:
- ALL
environment:
GOEMAXIMA_QUEUE_LEN: 32
read_only: true

api:
build:
context: ./../../
dockerfile: ./api/docker/Dockerfile
target: development
restart: unless-stopped
expose:
- "80"
volumes:
- ../../:/srv/stack
- /srv/stack/api/vendor/
depends_on:
- maxima

frontend:
build:
context: ./../../
dockerfile: ./api/docker/DockerfilePrivateDemo
restart: unless-stopped
environment:
STACK_PRIVATE_DEMO_API_URL: http://api
STACK_PRIVATE_DEMO_LIBRARY: stacklibrary
command: >
sh -c "test -f /srv/stack/api/private-demo/assets/question-manifest.json ||
php /srv/stack/api/private-demo/build_manifest.php;
apache2-foreground"
volumes:
- ../../api/private-demo:/srv/stack/api/private-demo
- ../../api/public/api.css:/srv/stack/api/private-demo/public/assets/api.css:ro
- ../../api/public/stackjsvle.js:/srv/stack/api/private-demo/public/assets/stackjsvle.js:ro
- ../../api/public/stackshared.js:/srv/stack/api/private-demo/public/assets/stackshared.js:ro
- ../../api/public/logo_large.png:/srv/stack/api/private-demo/public/assets/logo_large.png:ro
- ../../api/public/favicon.ico:/srv/stack/api/private-demo/public/favicon.ico:ro
- ../../samplequestions:/srv/stack/samplequestions:ro
- ../../corsscripts:/srv/stack/corsscripts:ro
ports:
- "3081:80"
depends_on:
- api
37 changes: 37 additions & 0 deletions api/docker/docker-compose.demo-private.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This demo exposes only the frontend container. The browser sends question ids
# to the frontend, and the frontend privately sends question XML to the API.
services:
maxima:
image: mathinstitut/goemaxima:2026062900-latest
tmpfs:
- "/tmp"
restart: unless-stopped
cap_add:
- SETGID
- SETUID
cap_drop:
- ALL
environment:
GOEMAXIMA_QUEUE_LEN: 32
read_only: true

api:
image: stackmaths/stackapi:2026062900-latest
restart: unless-stopped
expose:
- "80"
depends_on:
- maxima

frontend:
build:
context: ./../../
dockerfile: ./api/docker/DockerfilePrivateDemo
restart: unless-stopped
environment:
STACK_PRIVATE_DEMO_API_URL: http://api
STACK_PRIVATE_DEMO_LIBRARY: stacklibrary
ports:
- "80:80"
depends_on:
- api
13 changes: 13 additions & 0 deletions api/docker/files/private-demo/000-default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot ${APACHE_DOCUMENT_ROOT}

<Directory ${APACHE_DOCUMENT_ROOT}>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>

ErrorLog /dev/stderr
TransferLog /dev/stdout
</VirtualHost>
1 change: 1 addition & 0 deletions api/private-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
question-manifest.json
2 changes: 2 additions & 0 deletions api/private-demo/assets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
141 changes: 141 additions & 0 deletions api/private-demo/build_manifest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php
// This file is part of Stack - http://stack.maths.ed.ac.uk/
//
// Stack is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Stack is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Stack. If not, see <http://www.gnu.org/licenses/>.

/**
* Build the private demo question manifest.
*
* The browser receives opaque question ids from this manifest. Only the
* frontend container maps those ids back to question-library XML files.
*
* @package qtype_stack
* @copyright 2026 University of Edinburgh
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
*/
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/lib.php');

$root = STACK_PRIVATE_DEMO_LIBRARY_ROOT;
$output = __DIR__ . '/assets/question-manifest.json';
$libraryroot = 'samplequestions/' . getenv('STACK_PRIVATE_DEMO_LIBRARY');

if ($root === false || !is_dir($root)) {
fwrite(STDERR, "Could not find $libraryroot.\n");
exit(1);
}

$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
);

foreach ($iterator as $file) {
if ($file->isFile() && strtolower($file->getExtension()) === 'xml') {
$files[] = $file->getPathname();
}
}
sort($files, SORT_STRING);

$questions = [];
$errors = [];
$skipped = [
'nostack' => 0,
'multiple' => 0,
];

foreach ($files as $file) {
$relativepath = ltrim(str_replace('\\', '/', substr($file, strlen($root))), '/');
$xml = file_get_contents($file);

libxml_use_internal_errors(true);
$quiz = simplexml_load_string($xml);
$xmlerrors = libxml_get_errors();
libxml_clear_errors();

if ($quiz === false) {
$errors[] = $relativepath . ': invalid XML';
continue;
}

$stackquestions = [];
foreach ($quiz->question as $question) {
$type = (string) $question['type'];
if ($type === 'category') {
continue;
}
if ($type === 'stack') {
$stackquestions[] = $question;
}
}

if (count($stackquestions) === 0) {
// The library contains category metadata and Moodle question types that
// are not STACK API questions. They are not part of this demo catalogue.
$skipped['nostack']++;
continue;
}

if (count($stackquestions) !== 1) {
// This demo maps one public id to one library file. Multi-question
// bundles can stay in the library, but are skipped here.
$skipped['multiple']++;
continue;
}

$name = trim((string) $stackquestions[0]->name->text);
if ($name === '') {
$name = basename($relativepath, '.xml');
}

$id = 'q_' . substr(hash('sha256', $relativepath), 0, 16);
if (array_key_exists($id, $questions)) {
$errors[] = $relativepath . ': duplicate generated question id ' . $id;
continue;
}

$category = dirname($relativepath);
$questions[$id] = [
'id' => $id,
'name' => $name,
'filename' => basename($relativepath),
'path' => $relativepath,
'category' => $category === '.' ? '' : $category,
];
}

if ($errors) {
foreach ($errors as $error) {
fwrite(STDERR, $error . "\n");
}
}

$manifest = [
'root' => $libraryroot,
'questions' => $questions,
];

$json = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
if ($json === false) {
fwrite(STDERR, "Could not encode manifest JSON.\n");
exit(1);
}

file_put_contents($output, $json . "\n");
fwrite(STDOUT, 'Wrote ' . count($questions) . " questions to $output\n");
fwrite(
STDOUT,
'Skipped ' . $skipped['nostack'] . ' XML files without STACK questions and ' .
$skipped['multiple'] . " multi-question XML files.\n"
);
Loading
Loading