mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
i think i fixed it?
This commit is contained in:
@@ -48,6 +48,27 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
||||
api.storage.weightings = {};
|
||||
}
|
||||
|
||||
// Clear any stuck "processing" states so they can retry
|
||||
let hasStuckProcessing = false;
|
||||
for (const key in api.storage.weightings) {
|
||||
if (api.storage.weightings[key] === "processing") {
|
||||
delete api.storage.weightings[key];
|
||||
hasStuckProcessing = true;
|
||||
}
|
||||
}
|
||||
if (hasStuckProcessing) {
|
||||
// Force update storage
|
||||
api.storage.weightings = { ...api.storage.weightings };
|
||||
}
|
||||
|
||||
// Expose globally for easy access in console: window.BetterSEQTAWeightings
|
||||
(window as any).BetterSEQTAWeightings = api.storage.weightings;
|
||||
|
||||
// Keep it updated when weightings change
|
||||
api.storage.onChange('weightings', (newWeightings) => {
|
||||
(window as any).BetterSEQTAWeightings = newWeightings;
|
||||
});
|
||||
|
||||
api.seqta.onMount(".assessmentsWrapper", async () => {
|
||||
await waitForElm(
|
||||
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
|
||||
@@ -271,23 +292,308 @@ const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
||||
},
|
||||
};
|
||||
|
||||
async function extractPDFText(url: string): Promise<string> {
|
||||
// Fetch PDF as ArrayBuffer to avoid blob URL CSP issues in Firefox extensions
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
// Detect Firefox (has stricter CSP for blob URLs)
|
||||
// Use userAgent instead of deprecated InstallTrigger
|
||||
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 &&
|
||||
!navigator.userAgent.toLowerCase().includes('seamonkey') &&
|
||||
!navigator.userAgent.toLowerCase().includes('waterfox');
|
||||
|
||||
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
|
||||
// Detect if URL is a blob URL
|
||||
const isBlobUrl = url.startsWith('blob:');
|
||||
|
||||
const loadingTask = pdfjs.getDocument({ data: arrayBuffer });
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
let text = "";
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
||||
// For Firefox, ALWAYS use page context to avoid any CSP issues
|
||||
// For blob URLs in any browser, use page context
|
||||
if (isBlobUrl || isFirefox) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Inject script into page context to fetch (bypasses Firefox CSP restrictions)
|
||||
const script = document.createElement('script');
|
||||
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
|
||||
|
||||
// Escape URL for use in script
|
||||
const escapedUrl = url.replace(/'/g, "\\'");
|
||||
|
||||
script.textContent = `
|
||||
(function() {
|
||||
fetch('${escapedUrl}')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||
}
|
||||
return response.arrayBuffer();
|
||||
})
|
||||
.then(arrayBuffer => {
|
||||
window.postMessage({
|
||||
type: '${requestId}',
|
||||
success: true,
|
||||
data: Array.from(new Uint8Array(arrayBuffer))
|
||||
}, '*');
|
||||
})
|
||||
.catch(error => {
|
||||
window.postMessage({
|
||||
type: '${requestId}',
|
||||
success: false,
|
||||
error: error.message || String(error)
|
||||
}, '*');
|
||||
});
|
||||
})();
|
||||
`;
|
||||
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
if (event.data?.type === requestId) {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
|
||||
if (event.data.success) {
|
||||
// Convert back to ArrayBuffer
|
||||
const uint8Array = new Uint8Array(event.data.data);
|
||||
resolve(uint8Array.buffer);
|
||||
} else {
|
||||
reject(new Error(event.data.error || 'Failed to fetch PDF'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
reject(new Error('Timeout fetching PDF'));
|
||||
}, 30000);
|
||||
});
|
||||
} else {
|
||||
// Regular URL - fetch normally, but check if response URL becomes blob
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include',
|
||||
redirect: 'follow',
|
||||
});
|
||||
|
||||
// Check if response URL is a blob URL (server might redirect to blob)
|
||||
if (response.url && response.url.startsWith('blob:')) {
|
||||
// Re-fetch using page context
|
||||
return await fetchPDFAsArrayBuffer(response.url);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch PDF: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.arrayBuffer();
|
||||
} catch (error: any) {
|
||||
// If error mentions blob or security, try using page context
|
||||
if (error?.message?.includes('blob') || error?.message?.includes('Security') || error?.message?.includes('CSP')) {
|
||||
// Force use page context
|
||||
return await fetchPDFAsArrayBuffer(url);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
async function extractPDFText(url: string): Promise<string> {
|
||||
// For Firefox, do everything in page context to avoid blob URL CSP issues
|
||||
if (isFirefox) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
|
||||
|
||||
// Escape URL for use in script (handle both single and double quotes)
|
||||
const escapedUrl = url.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
||||
|
||||
script.textContent = `
|
||||
(function() {
|
||||
const requestId = '${requestId}';
|
||||
const url = '${escapedUrl}';
|
||||
|
||||
// Check if pdfjs is already loaded
|
||||
if (window.pdfjsLib) {
|
||||
extractPDF();
|
||||
} else {
|
||||
// Load pdfjs in page context
|
||||
const pdfjsScript = document.createElement('script');
|
||||
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.mjs';
|
||||
pdfjsScript.type = 'module';
|
||||
|
||||
pdfjsScript.onload = function() {
|
||||
extractPDF();
|
||||
};
|
||||
pdfjsScript.onerror = function() {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Failed to load pdfjs library'
|
||||
}, '*');
|
||||
};
|
||||
|
||||
document.head.appendChild(pdfjsScript);
|
||||
}
|
||||
|
||||
function extractPDF() {
|
||||
try {
|
||||
// Disable worker for Firefox to avoid blob URL CSP issues
|
||||
// Set to empty string to disable worker completely
|
||||
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
|
||||
|
||||
// Use XMLHttpRequest instead of fetch for better blob URL handling
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.withCredentials = true;
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status !== 200) {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'HTTP ' + xhr.status + ': ' + xhr.statusText
|
||||
}, '*');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const arrayBuffer = xhr.response;
|
||||
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
|
||||
throw new Error('PDF response is empty');
|
||||
}
|
||||
|
||||
window.pdfjsLib.getDocument({
|
||||
data: arrayBuffer,
|
||||
useSystemFonts: true,
|
||||
verbosity: 0,
|
||||
// Explicitly disable worker
|
||||
useWorkerFetch: false,
|
||||
isEvalSupported: false
|
||||
}).promise
|
||||
.then(pdf => {
|
||||
const pagePromises = [];
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
pagePromises.push(
|
||||
pdf.getPage(i).then(page => {
|
||||
return page.getTextContent().then(content => {
|
||||
return content.items.map(item => item.str).join(' ');
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.all(pagePromises);
|
||||
})
|
||||
.then(pages => {
|
||||
const text = pages.join('\\n');
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: true,
|
||||
text: text
|
||||
}, '*');
|
||||
})
|
||||
.catch(error => {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'PDF parsing error: ' + (error.message || String(error))
|
||||
}, '*');
|
||||
});
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'ArrayBuffer error: ' + (error.message || String(error))
|
||||
}, '*');
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Network error fetching PDF'
|
||||
}, '*');
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Timeout fetching PDF'
|
||||
}, '*');
|
||||
};
|
||||
|
||||
xhr.timeout = 30000;
|
||||
xhr.send();
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Setup error: ' + (error.message || String(error))
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
if (event.data?.type === requestId) {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
|
||||
if (event.data.success) {
|
||||
resolve(event.data.text);
|
||||
} else {
|
||||
reject(new Error(event.data.error || 'Failed to extract PDF text'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
|
||||
// Timeout after 60 seconds (PDF parsing can take time)
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
reject(new Error('Timeout extracting PDF text'));
|
||||
}, 60000);
|
||||
});
|
||||
} else {
|
||||
// Chrome - use extension context
|
||||
try {
|
||||
const arrayBuffer = await fetchPDFAsArrayBuffer(url);
|
||||
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
throw new Error('PDF response is empty');
|
||||
}
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: arrayBuffer,
|
||||
useSystemFonts: true,
|
||||
});
|
||||
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
let text = "";
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
||||
}
|
||||
|
||||
return text;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWeightings(mark: any, api: any) {
|
||||
@@ -295,45 +601,85 @@ async function handleWeightings(mark: any, api: any) {
|
||||
const metaclassID = mark.metaclassID;
|
||||
const userInfo = await getUserInfo();
|
||||
const userID = userInfo.id;
|
||||
if (api.storage.weightings[assessmentID] != undefined) {
|
||||
|
||||
// Skip if already processed (not "processing")
|
||||
if (api.storage.weightings[assessmentID] != undefined && api.storage.weightings[assessmentID] !== "processing") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set to processing
|
||||
api.storage.weightings = {
|
||||
...api.storage.weightings,
|
||||
[assessmentID]: "processing",
|
||||
};
|
||||
|
||||
const filename =
|
||||
"BetterSEQTA-" + String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
||||
try {
|
||||
const filename =
|
||||
"BetterSEQTA-" + String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
||||
|
||||
await fetch(`${location.origin}/seqta/student/print/assessment`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify({
|
||||
fileName: filename,
|
||||
id: assessmentID,
|
||||
metaclass: metaclassID,
|
||||
student: userID,
|
||||
}),
|
||||
});
|
||||
const printResponse = await fetch(`${location.origin}/seqta/student/print/assessment`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
fileName: filename,
|
||||
id: assessmentID,
|
||||
metaclass: metaclassID,
|
||||
student: userID,
|
||||
}),
|
||||
});
|
||||
|
||||
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
||||
const text = await extractPDFText(pdfUrl);
|
||||
if (!printResponse.ok) {
|
||||
throw new Error(`Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`);
|
||||
}
|
||||
|
||||
// Use regex to find the line "Assessment weight: X"
|
||||
const match = text.match(/Assessment weight:\s*(\d+\.?\d*)/i);
|
||||
const weight = match ? match[1] : "N/A";
|
||||
// Wait a bit for the PDF to be generated
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Save it to storage
|
||||
api.storage.weightings = {
|
||||
...api.storage.weightings,
|
||||
[assessmentID]: weight,
|
||||
};
|
||||
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
||||
|
||||
// Check if URL is a blob URL (which extensions can't access)
|
||||
if (pdfUrl.startsWith('blob:')) {
|
||||
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
|
||||
}
|
||||
|
||||
let text: string;
|
||||
try {
|
||||
// For Firefox, extractPDFText already handles everything in page context
|
||||
// For Chrome, it uses extension context
|
||||
text = await extractPDFText(pdfUrl);
|
||||
} catch (error: any) {
|
||||
// Handle CSP errors or other fetch issues
|
||||
// Suppress Firefox blob URL CSP errors (they're warnings, not fatal)
|
||||
if (isFirefox && (error?.message?.includes('blob') || error?.message?.includes('Security') || error?.message?.includes('CSP'))) {
|
||||
// Try one more time with a longer delay in case PDF wasn't ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
try {
|
||||
text = await extractPDFText(pdfUrl);
|
||||
} catch (retryError: any) {
|
||||
throw new Error(`PDF extraction failed after retry: ${retryError.message}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`PDF extraction failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Assessment ID ${assessmentID} weight:`, weight);
|
||||
// Use regex to find the line "Assessment weight: X"
|
||||
const match = text.match(/Assessment weight:\s*(\d+\.?\d*)/i);
|
||||
const weight = match ? match[1] : "N/A";
|
||||
|
||||
console.log(text);
|
||||
// Save it to storage
|
||||
api.storage.weightings = {
|
||||
...api.storage.weightings,
|
||||
[assessmentID]: weight,
|
||||
};
|
||||
} catch (error: any) {
|
||||
// Catch any error and set to N/A instead of leaving as "processing"
|
||||
api.storage.weightings = {
|
||||
...api.storage.weightings,
|
||||
[assessmentID]: "N/A",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function parseAssessments(api: any) {
|
||||
|
||||
Reference in New Issue
Block a user