mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-11 10:05:02 +00:00

# Description Please provide a summary of the changes, including relevant motivation and context. Closes https://github.com/Stirling-Tools/Stirling-PDF/security/code-scanning/164 ## Checklist - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have performed a self-review of my own code - [ ] I have attached images of the change if it is UI based - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) - [x] My changes generate no new warnings - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
512 lines
17 KiB
HTML
512 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
|
<head>
|
|
<th:block th:insert="~{fragments/common :: head(title=#{releases.title}, header=#{releases.title})}"></th:block>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="page-container">
|
|
<div id="content-wrap">
|
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
|
<br><br>
|
|
<div class="container">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8 bg-card">
|
|
<div class="tool-header">
|
|
<span class="material-symbols-rounded tool-header-icon history">update</span>
|
|
<span class="tool-header-text" th:text="#{releases.header}">Release Notes</span>
|
|
</div>
|
|
|
|
<div class="alert alert-info" role="alert">
|
|
<strong th:text="#{releases.current.version}">Current Installed Version</strong>:
|
|
<span id="currentVersion" th:text="${@appVersion}"></span>
|
|
</div>
|
|
|
|
<div class="alert alert-warning" role="alert">
|
|
<span th:text="#{releases.note}">All release notes are only available in English</span>
|
|
</div>
|
|
|
|
<div id="loading" class="text-center my-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="error-message" class="alert alert-danger d-none" role="alert">
|
|
Failed to load release notes. Please try again later.
|
|
</div>
|
|
|
|
<!-- Release Notes Container -->
|
|
<div id="release-notes-container" class="release-notes-container">
|
|
<!-- Release notes will be dynamically inserted here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
</div>
|
|
|
|
<style>
|
|
.release-notes-container {
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.release-card {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 1.5rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.release-card.current-version {
|
|
border-color: #28a745;
|
|
background-color: rgba(40, 167, 69, 0.05);
|
|
}
|
|
|
|
.release-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.release-header h3 {
|
|
margin: 0;
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.version {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.release-date {
|
|
color: #6c757d;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.release-body {
|
|
font-size: 0.9rem;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.release-link {
|
|
color: #0d6efd;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.release-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
|
|
<script th:inline="javascript">
|
|
/*<![CDATA[*/
|
|
|
|
// Get the current version from the appVersion bean
|
|
const appVersion = /*[[${@appVersion}]]*/ '';
|
|
|
|
// GitHub API configuration
|
|
const REPO_OWNER = 'Stirling-Tools';
|
|
const REPO_NAME = 'Stirling-PDF';
|
|
const GITHUB_API = 'https://api.github.com/repos/' + REPO_OWNER + '/' + REPO_NAME;
|
|
const GITHUB_URL = 'https://github.com/' + REPO_OWNER + '/' + REPO_NAME;
|
|
const MAX_RELEASES = 8;
|
|
|
|
// Secure element creation helper
|
|
function createElement(tag, attributes = {}, children = []) {
|
|
const element = document.createElement(tag);
|
|
Object.entries(attributes).forEach(([key, value]) => {
|
|
if (typeof value === 'string') {
|
|
element.setAttribute(key, value);
|
|
}
|
|
});
|
|
children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
element.appendChild(document.createTextNode(child));
|
|
} else if (child instanceof Node) {
|
|
element.appendChild(child);
|
|
}
|
|
});
|
|
return element;
|
|
}
|
|
|
|
const ALLOWED_TAGS = {
|
|
'a': ['href', 'target', 'rel', 'class'],
|
|
'img': ['src', 'alt', 'width', 'height', 'style'],
|
|
'br': [],
|
|
'p': [],
|
|
'div': [],
|
|
'span': []
|
|
};
|
|
|
|
// Function to safely create HTML elements from string
|
|
function createSafeElement(htmlString) {
|
|
try {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(htmlString, 'text/html');
|
|
|
|
function sanitizeNode(node) {
|
|
// Safety check for null/undefined
|
|
if (!node) return null;
|
|
|
|
// Handle text nodes
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
return node.cloneNode(true);
|
|
}
|
|
|
|
// Handle element nodes
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const tagName = node.tagName.toLowerCase();
|
|
|
|
// Check if tag is allowed
|
|
if (!ALLOWED_TAGS[tagName]) {
|
|
return document.createTextNode(node.textContent);
|
|
}
|
|
|
|
// Create new element
|
|
const cleanElement = document.createElement(tagName);
|
|
|
|
// Copy allowed attributes
|
|
const allowedAttributes = ALLOWED_TAGS[tagName];
|
|
Array.from(node.attributes).forEach(attr => {
|
|
if (allowedAttributes.includes(attr.name)) {
|
|
let value = attr.value;
|
|
if (attr.name === 'href' || attr.name === 'src') {
|
|
try {
|
|
value = encodeURI(value);
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
cleanElement.setAttribute(attr.name, value);
|
|
}
|
|
});
|
|
|
|
// Add security attributes for links
|
|
if (tagName === 'a') {
|
|
cleanElement.setAttribute('rel', 'noopener noreferrer');
|
|
}
|
|
|
|
// Process children
|
|
Array.from(node.childNodes).forEach(child => {
|
|
const cleanChild = sanitizeNode(child);
|
|
if (cleanChild) {
|
|
cleanElement.appendChild(cleanChild);
|
|
}
|
|
});
|
|
|
|
return cleanElement;
|
|
}
|
|
|
|
// If not text or element node, return null
|
|
return null;
|
|
}
|
|
|
|
// Get the actual content from the body
|
|
const content = doc.body.children;
|
|
if (!content || content.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// If it's a single element, process it directly
|
|
if (content.length === 1) {
|
|
return sanitizeNode(content[0]);
|
|
}
|
|
|
|
// If multiple elements, wrap them in a div
|
|
const wrapper = document.createElement('div');
|
|
Array.from(content).forEach(node => {
|
|
const cleanNode = sanitizeNode(node);
|
|
if (cleanNode) {
|
|
wrapper.appendChild(cleanNode);
|
|
}
|
|
});
|
|
|
|
return wrapper;
|
|
} catch (error) {
|
|
console.error('Error parsing HTML:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function processGitHubReferences(text) {
|
|
if (!text || typeof text !== 'string') {
|
|
return text;
|
|
}
|
|
|
|
let currentText = text;
|
|
let match;
|
|
let lastIndex = 0;
|
|
const result = document.createElement('span');
|
|
const urlRegex = new RegExp('https://github\\.com/' + REPO_OWNER + '/' + REPO_NAME + '/(?:issues|pull)/(\\d+)', 'g');
|
|
|
|
while ((match = urlRegex.exec(text)) !== null) {
|
|
// Add text before the match
|
|
if (match.index > lastIndex) {
|
|
result.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
|
|
}
|
|
|
|
// Create link element
|
|
const link = document.createElement('a');
|
|
link.href = encodeURI(match[0]);
|
|
link.textContent = `#${match[1]}`; // Use issue/PR number
|
|
link.className = 'release-link';
|
|
link.target = '_blank';
|
|
link.rel = 'noopener noreferrer';
|
|
result.appendChild(link);
|
|
|
|
lastIndex = match.index + match[0].length;
|
|
}
|
|
|
|
// Add remaining text after last match
|
|
if (lastIndex < text.length) {
|
|
result.appendChild(document.createTextNode(text.substring(lastIndex)));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Update formatText function to handle processGitHubReferences properly
|
|
function formatText(text) {
|
|
const container = document.createElement('div');
|
|
|
|
if (!text || typeof text !== 'string') {
|
|
console.error('Invalid input to formatText:', text);
|
|
return container;
|
|
}
|
|
|
|
let textWithoutComments;
|
|
try {
|
|
textWithoutComments = text.replace(
|
|
/<!-- Release notes generated using configuration in .github\/release\.yml at main -->/,
|
|
''
|
|
);
|
|
} catch (error) {
|
|
console.error('Error in replace operation:', error);
|
|
return container;
|
|
}
|
|
|
|
// Split the text into lines
|
|
let lines;
|
|
try {
|
|
lines = textWithoutComments.split('\n');
|
|
} catch (error) {
|
|
console.error('Error in split operation:', error);
|
|
return container;
|
|
}
|
|
|
|
let currentList = null;
|
|
|
|
lines.forEach(line => {
|
|
const trimmedLine = line.trim();
|
|
|
|
// Skip empty lines but add spacing
|
|
if (!trimmedLine) {
|
|
if (currentList) {
|
|
container.appendChild(currentList);
|
|
currentList = null;
|
|
}
|
|
container.appendChild(document.createElement('br'));
|
|
return;
|
|
}
|
|
|
|
// Check if the line is HTML
|
|
if (trimmedLine.startsWith('<') && trimmedLine.endsWith('>')) {
|
|
if (currentList) {
|
|
container.appendChild(currentList);
|
|
currentList = null;
|
|
}
|
|
|
|
const safeElement = createSafeElement(trimmedLine);
|
|
if (safeElement) {
|
|
container.appendChild(safeElement);
|
|
} else {
|
|
// If HTML parsing fails, treat as plain text
|
|
container.appendChild(document.createTextNode(trimmedLine));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for headers
|
|
const headerMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/);
|
|
if (headerMatch) {
|
|
if (currentList) {
|
|
container.appendChild(currentList);
|
|
currentList = null;
|
|
}
|
|
const headerLevel = headerMatch[1].length;
|
|
const headerContent = headerMatch[2];
|
|
// Process GitHub references in headers
|
|
const processedContent = processGitHubReferences(headerContent);
|
|
const header = createElement(`h${headerLevel}`);
|
|
header.appendChild(processedContent);
|
|
container.appendChild(header);
|
|
return;
|
|
}
|
|
|
|
// Check for bullet points
|
|
const bulletMatch = trimmedLine.match(/^[-*]\s+(.+)$/);
|
|
if (bulletMatch) {
|
|
if (!currentList) {
|
|
currentList = document.createElement('ul');
|
|
}
|
|
|
|
const listContent = bulletMatch[1];
|
|
const listItem = document.createElement('li');
|
|
|
|
// Process GitHub references in list items
|
|
listItem.appendChild(processGitHubReferences(listContent));
|
|
currentList.appendChild(listItem);
|
|
return;
|
|
}
|
|
|
|
// If we reach here and have a list, append it
|
|
if (currentList) {
|
|
container.appendChild(currentList);
|
|
currentList = null;
|
|
}
|
|
|
|
// Handle regular paragraph
|
|
const paragraph = document.createElement('p');
|
|
paragraph.appendChild(processGitHubReferences(trimmedLine));
|
|
container.appendChild(paragraph);
|
|
});
|
|
|
|
// Append any remaining list
|
|
if (currentList) {
|
|
container.appendChild(currentList);
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
const MAX_PREVIOUS_RELEASES = 5;
|
|
function compareVersions(v1, v2) {
|
|
const normalize = v => v.replace(/^v/, '');
|
|
const v1Parts = normalize(v1).split('.').map(Number);
|
|
const v2Parts = normalize(v2).split('.').map(Number);
|
|
|
|
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
|
const v1Part = v1Parts[i] || 0;
|
|
const v2Part = v2Parts[i] || 0;
|
|
if (v1Part > v2Part) return 1;
|
|
if (v1Part < v2Part) return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
async function loadReleases() {
|
|
const container = document.getElementById('release-notes-container');
|
|
const loading = document.getElementById('loading');
|
|
const errorMessage = document.getElementById('error-message');
|
|
|
|
try {
|
|
loading.classList.remove('d-none');
|
|
errorMessage.classList.add('d-none');
|
|
|
|
// Clear container safely
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
const cachedReleases = sessionStorage.getItem('releases');
|
|
|
|
let releases;
|
|
if (cachedReleases) {
|
|
releases = JSON.parse(cachedReleases);
|
|
console.log("Read from storage");
|
|
} else {
|
|
const response = await fetch(GITHUB_API + '/releases');
|
|
if (!response.ok) {
|
|
if (response.status === 403) {
|
|
throw new Error('API rate limit exceeded');
|
|
}
|
|
if (response.status === 404) {
|
|
throw new Error('Repository not found');
|
|
}
|
|
throw new Error('Failed to fetch releases');
|
|
}
|
|
releases = await response.json();
|
|
sessionStorage.setItem('releases', JSON.stringify(releases));
|
|
}
|
|
|
|
// Sort releases by version number (descending)
|
|
releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name));
|
|
|
|
// Find index of current version
|
|
const currentVersionIndex = releases.findIndex(release =>
|
|
compareVersions(release.tag_name, 'v' + appVersion) === 0 ||
|
|
compareVersions(release.tag_name, appVersion) === 0
|
|
);
|
|
|
|
if (currentVersionIndex === -1) {
|
|
container.appendChild(createElement('div', {
|
|
class: 'alert alert-warning'
|
|
}, ['Current version not found in releases.']));
|
|
return;
|
|
}
|
|
|
|
// Get current version and 8 previous releases
|
|
const endIndex = Math.min(currentVersionIndex + MAX_PREVIOUS_RELEASES + 1, releases.length);
|
|
const relevantReleases = releases.slice(currentVersionIndex, endIndex);
|
|
|
|
if (relevantReleases.length === 0) {
|
|
container.appendChild(createElement('div', {
|
|
class: 'alert alert-warning'
|
|
}, ['No releases found.']));
|
|
return;
|
|
}
|
|
|
|
relevantReleases.forEach((release, index) => {
|
|
const isCurrentVersion = index === 0; // First release in the array is current version
|
|
|
|
const releaseCard = createElement('div', {
|
|
class: `release-card ${isCurrentVersion ? 'current-version' : ''}`
|
|
});
|
|
|
|
const header = createElement('div', { class: 'release-header' });
|
|
|
|
const h3 = createElement('h3', {}, [
|
|
createElement('span', { class: 'version' }, [release.tag_name]),
|
|
createElement('span', { class: 'release-date' }, [
|
|
new Date(release.created_at).toLocaleDateString()
|
|
])
|
|
]);
|
|
|
|
header.appendChild(h3);
|
|
|
|
if (isCurrentVersion) {
|
|
header.appendChild(createElement('span', {
|
|
class: 'badge bg-success'
|
|
}, ['Installed']));
|
|
}
|
|
|
|
releaseCard.appendChild(header);
|
|
|
|
const body = createElement('div', { class: 'release-body' });
|
|
body.appendChild(formatText(release.body || 'No release notes available.'));
|
|
|
|
releaseCard.appendChild(body);
|
|
container.appendChild(releaseCard);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading releases:', error);
|
|
errorMessage.classList.remove('d-none');
|
|
} finally {
|
|
loading.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
|
|
// Load releases when the page loads
|
|
document.addEventListener('DOMContentLoaded', loadReleases);
|
|
|
|
/*]]>*/
|
|
</script>
|
|
</body>
|
|
</html>
|