Files
WebP-eXpress/lib/options/js/bulk-convert.js
Malin 37cf714058 WebP Express CloudHost.es Fix v0.25.9-cloudhost
 Fixed bulk conversion getting stuck on missing files
 Added robust error handling and timeout protection
 Improved JavaScript response parsing
 Added file existence validation
 Fixed missing PHP class imports
 Added comprehensive try-catch error recovery

🔧 Key fixes:
- File existence checks before conversion attempts
- 30-second timeout protection per file
- Graceful handling of 500 errors and JSON parsing issues
- Automatic continuation to next file on failures
- Cache busting for JavaScript updates

🎯 Result: Bulk conversion now completes successfully even with missing files

🚀 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-23 10:22:32 +02:00

486 lines
19 KiB
JavaScript

function openBulkConvertPopup() {
document.getElementById('bulkconvertlog').innerHTML = '';
document.getElementById('bulkconvertcontent').innerHTML = '<div>Receiving list of files to convert...</div>';
tb_show('Bulk Convert', '#TB_inline?inlineId=bulkconvertpopup');
var data = {
'action': 'list_unconverted_files',
'nonce' : window.webpExpress['ajax-nonces']['list-unconverted-files'],
};
jQuery.ajax({
type: "POST",
url: ajaxurl,
data: data,
dataType: 'text',
timeout: 30000,
error: function (jqXHR, status, errorThrown) {
html = '<h1>Error: ' + status + '</h1>';
html += errorThrown;
document.getElementById('bulkconvertcontent').innerHTML = html;
},
success: function(response) {
if ((typeof response == 'object') && (response['success'] == false)) {
html = '<h1>Error</h1>';
if (response['data'] && ((typeof response['data']) == 'string')) {
html += webpexpress_escapeHTML(response['data']);
}
document.getElementById('bulkconvertcontent').innerHTML = html;
return;
}
if (response == '') {
html = '<h1>Error</h1>';
html += '<p>Could not fetch list of files to convert. The server returned nothing (which is unexpected - ' +
'it is not simply because there are no files to convert.)</p>';
document.getElementById('bulkconvertcontent').innerHTML = html;
return;
}
var responseObj;
try {
responseObj = JSON.parse(response);
} catch (e) {
html = '<h1>Error</h1>';
html += '<p>The ajax call did not return valid JSON, as expected.</p>';
html += '<p>Check the javascript console to see what was returned.</p>';
console.log('The ajax call did not return valid JSON, as expected');
console.log('Here is what was received:');
console.log(response);
document.getElementById('bulkconvertcontent').innerHTML = html;
return;
}
var bulkInfo = {
'groups': responseObj,
'groupPointer': 0,
'filePointer': 0,
'paused': false,
'webpTotalFilesize': 0,
'orgTotalFilesize': 0,
};
window.webpexpress_bulkconvert = bulkInfo;
// count files
var numFiles = 0;
for (var i=0; i<bulkInfo.groups.length; i++) {
numFiles += bulkInfo.groups[i].files.length;
}
//console.log(JSON.parse(response));
var html = '';
if (numFiles == 0) {
html += '<p>There are no unconverted files</p>';
} else {
html += '<div>'
html += '<p>There are ' + numFiles + ' unconverted files.</p>';
html += '<p><i>Note that in a typical setup, you will have redirect rules which trigger conversion when needed, ' +
'and thus you have no need for bulk conversion. In fact, in that case, you should probably not bulk convert ' +
'because bulk conversion will also convert images and thumbnails which are not in use, and thus take up ' +
'more disk space than necessary. The bulk conversion feature was only added in order to make the plugin usable even when ' +
'there are problems with redirects (ie on Nginx in case you do not have access to the config or on Microsoft IIS). ' +
'</i></p><br>';
html += '<button onclick="startBulkConversion()" class="button button-primary" type="button">Start conversion</button>';
html += '</div>';
}
document.getElementById('bulkconvertcontent').innerHTML = html;
}
});
}
function pauseBulkConversion() {
var bulkInfo = window.webpexpress_bulkconvert;
bulkInfo.paused = true;
}
function pauseOrResumeBulkConversion() {
var bulkInfo = window.webpexpress_bulkconvert;
bulkInfo.paused = !bulkInfo.paused;
document.getElementById('bulkPauseResumeBtn').innerText = (bulkInfo.paused ? 'Resume' : 'Pause');
if (!bulkInfo.paused) {
convertNextInBulkQueue();
}
}
function startBulkConversion() {
var html = '<br>';
html += '<style>' +
'.has-tip {cursor:pointer; position:static;}\n' +
'.has-tip .tip {display: none}\n' +
'.has-tip:hover .tip {display: block}\n' +
'.tip{padding: 5px 10px; background-color:#ff9;min-width:310px; font-size:10px; color: black; border:1px solid black; max-width:90%;z-index:10}\n' +
'.reduction {float:right;}\n' +
'</style>';
html += '<button id="bulkPauseResumeBtn" onclick="pauseOrResumeBulkConversion()" class="button button-primary" type="button">Pause</button>';
//html += '<div id="conversionlog" class="das-popup">test</div>';
//html += '<div id="bulkconvertlog"></div>';
document.getElementById('bulkconvertcontent').innerHTML = html;
document.getElementById('bulkconvertlog').innerHTML = '';
convertNextInBulkQueue();
}
function convertDone() {
var bulkInfo = window.webpexpress_bulkconvert;
document.getElementById('bulkconvertlog').innerHTML += '<p><b>Done!</b></p>' +
'<p>Total reduction: ' + getReductionHtml(bulkInfo['orgTotalFilesize'], bulkInfo['webpTotalFilesize'], 'Total size of converted originals', 'Total size of converted webp files') + '</p>'
document.getElementById('bulkPauseResumeBtn').style.display = 'none';
}
function getPrintableSizeInfo(orgSize, webpSize) {
if (orgSize < 10000) {
return {
'org': orgSize + ' bytes',
'webp': webpSize + ' bytes'
};
} else {
return {
'org': Math.round(orgSize / 1024) + ' kb',
'webp': Math.round(webpSize / 1024) + ' kb'
};
}
}
function getReductionHtml(orgSize, webpSize, sizeOfOriginalText, sizeOfWebpText) {
var reduction = Math.round((orgSize - webpSize)/orgSize * 100);
var sizeInfo = getPrintableSizeInfo(orgSize, webpSize);
var hoverText = sizeOfOriginalText + ': ' + sizeInfo['org'] + '.<br>' + sizeOfWebpText + ': ' + sizeInfo['webp'];
// ps: this is all safe to print
return '<span class="has-tip reduction">' + reduction + '%' +
'<span class="tip">' + hoverText + '</span>' +
'</span><br>';
}
function logLn() {
var html = '';
for (i = 0; i < arguments.length; i++) {
html += arguments[i];
}
var spanEl = document.createElement('span');
spanEl.innerHTML = html;
document.getElementById('bulkconvertlog').appendChild(spanEl);
//document.getElementById('bulkconvertlog').innerHTML += html;
}
function webpexpress_viewLog(groupPointer, filePointer) {
/*
disabled until I am certain that security is in place.
var bulkInfo = window.webpexpress_bulkconvert;
var group = bulkInfo.groups[groupPointer];
var filename = group.files[filePointer];
var source = group.root + '/' + filename;
var w = Math.min(1200, Math.max(200, document.documentElement.clientWidth - 100));
var h = Math.max(250, document.documentElement.clientHeight - 80);
document.getElementById('conversionlog_content').innerHTML = 'loading log...'; // + source;
jQuery.ajax({
method: 'POST',
url: ajaxurl,
data: {
'action': 'webpexpress_view_log',
'nonce' : window.webpExpress['ajax-nonces']['view-log'],
'source': source
},
success: (response) => {
//alert(response);
if ((typeof response == 'object') && (response['success'] == false)) {
html = '<h1>Error</h1>';
if (response['data'] && ((typeof response['data']) == 'string')) {
html += webpexpress_escapeHTML(response['data']);
}
document.getElementById('conversionlog_content').innerHTML = html;
return;
}
var result = JSON.parse(response);
// the "log" result is a simply form of markdown, using just italic, bold and newlines.
// It ought not to return anything evil, but for good practice, let us encode.
result = webpexpress_escapeHTML(result);
var html = '<h1>Conversion log</h1><br>' + '<pre style="white-space:pre-wrap">' + result + '</pre>';
document.getElementById('conversionlog_content').innerHTML = html;
},
error: () => {
//responseCallback({requestError: true});
},
});
//<h1>Conversion log</h1>
//tb_show('Conversion log', '#TB_inline?inlineId=conversionlog');
openDasPopup('conversionlog', w, h);
*/
}
function convertNextInBulkQueue() {
var html;
var bulkInfo = window.webpexpress_bulkconvert;
//console.log('convertNextInBulkQueue', bulkInfo);
// Current group might contain 0, - skip if that is the case
while ((bulkInfo.groupPointer < bulkInfo.groups.length) && (bulkInfo.filePointer >= bulkInfo.groups[bulkInfo.groupPointer].files.length)) {
logLn(
'<h3>' + bulkInfo.groups[bulkInfo.groupPointer].groupName + '</h3>',
'<p>Nothing to convert</p>'
);
bulkInfo.groupPointer++;
bulkInfo.filePointer = 0;
}
if (bulkInfo.groupPointer >= bulkInfo.groups.length) {
convertDone();
return;
}
var group = bulkInfo.groups[bulkInfo.groupPointer];
var filename = group.files[bulkInfo.filePointer];
if (bulkInfo.filePointer == 0) {
logLn('<h3>' + group.groupName + '</h3>');
}
logLn('Converting <i>' + filename + '</i>');
var data = {
'action': 'convert_file',
'nonce' : window.webpExpress['ajax-nonces']['convert'],
'filename': group.root + '/' + filename
//'whatever': ajax_object.we_value // We pass php values differently!
};
function responseCallback(response){
try {
if ((typeof response == 'object') && (response['success'] == false)) {
html = '<h1>Error</h1>';
if (response['data'] && ((typeof response['data']) == 'string')) {
// disabled. Need to check if it is secure
//html += webpexpress_escapeHTML(response['data']);
}
logLn(html);
return
}
var result;
// Handle different types of responses safely
if (typeof response.requestError === 'boolean' && response.requestError) {
result = {
success: false,
msg: 'Request failed',
log: '',
};
} else if (typeof response === 'string') {
try {
result = JSON.parse(response);
} catch (e) {
result = {
success: false,
msg: 'Invalid response received from server',
log: '',
};
}
} else if (typeof response === 'object' && response !== null) {
// If it's already an object, check if it has the expected structure
if (typeof response.success !== 'undefined') {
result = response;
} else {
result = {
success: false,
msg: 'Invalid object response received from server',
log: '',
};
}
} else {
result = {
success: false,
msg: 'Unexpected response type: ' + typeof response,
log: '',
};
}
var bulkInfo = window.webpexpress_bulkconvert;
if (!bulkInfo || !bulkInfo.groups || bulkInfo.groupPointer >= bulkInfo.groups.length) {
logLn('<span style="color:red">Bulk conversion state is invalid</span><br>');
convertDone();
return;
}
var group = bulkInfo.groups[bulkInfo.groupPointer];
if (!group || !group.files || bulkInfo.filePointer >= group.files.length) {
logLn('<span style="color:red">Group or file index is invalid</span><br>');
convertDone();
return;
}
var filename = group.files[bulkInfo.filePointer];
//console.log(result);
var html = '';
var htmlViewLog = '';
// uncommented until I'm certain that security is in place
//var htmlViewLog = '&nbsp;&nbsp;<a style="cursor:pointer" onclick="webpexpress_viewLog(' + bulkInfo.groupPointer + ',' + bulkInfo.filePointer + ')">view log</a>';
if (result['success']) {
//console.log('nonce tick:' + result['nonce-tick']);
if (result['new-convert-nonce']) {
//console.log('new convert nonce:' + result['new-convert-nonce']);
window.webpExpress['ajax-nonces']['convert'] = result['new-convert-nonce'];
}
var orgSize = result['filesize-original'];
var webpSize = result['filesize-webp'];
var orgSizePrint, webpSizePrint;
bulkInfo['orgTotalFilesize'] += orgSize;
bulkInfo['webpTotalFilesize'] += webpSize;
//'- Saved at: ' + result['destination-path'] +
/*
html += ' <span style="color:green">ok</span></span>' +
htmlViewLog +
getReductionHtml(orgSize, webpSize, 'Size of original', 'Size of webp')*/
html += ' <span style="color:green" class="has-tip">ok' +
'<span class="tip">' +
'<b>Destination:</b><br>' + result['destination-path'] + '<br><br>' +
'<b>Url:</b><br><a href="' + result['destination-url'] + '">' + result['destination-url'] + '<br>' +
'</span>' +
'</span>' +
getReductionHtml(orgSize, webpSize, 'Size of original', 'Size of webp')
} else {
html += ' <span style="color:red">failed</span>' + htmlViewLog;
if (result['msg']) {
// Show the error message but continue processing
html += '<br><span style="color:red; font-size:13px">' + webpexpress_escapeHTML(result['msg']) + '</span>';
}
// Only stop for critical errors (security nonce issues), not file-specific errors
if (result['stop'] && result['msg'] && result['msg'].indexOf('security nonce') !== -1) {
logLn(html);
logLn('<br><span style="color:red; font-weight:bold;">Bulk conversion stopped due to security error. Please reload the page (F5) and try again.</span>');
return;
}
html += '<br>';
}
logLn(html);
// Get next
bulkInfo.filePointer++;
if (bulkInfo.filePointer == group.files.length) {
bulkInfo.filePointer = 0;
bulkInfo.groupPointer++;
}
if (bulkInfo.groupPointer == bulkInfo.groups.length) {
convertDone();
} else {
if (bulkInfo.paused) {
document.getElementById('bulkconvertlog').innerHTML += '<p><i>on pause</i><br>' +
'Reduction this far: ' + getReductionHtml(bulkInfo['orgTotalFilesize'], bulkInfo['webpTotalFilesize'], 'Total size of originals this far', 'Total size of webp files this far') + '</p>'
bulkInfo['orgTotalFilesize'] += orgSize;
bulkInfo['webpTotalFilesize'] += webpSize;
} else {
convertNextInBulkQueue();
}
}
} catch (error) {
// Catch any unexpected errors in responseCallback
logLn(' <span style="color:red">JavaScript error in response processing: ' + error.message + '</span><br>');
// Try to continue with next file
var bulkInfo = window.webpexpress_bulkconvert;
if (bulkInfo && bulkInfo.groups && bulkInfo.groupPointer < bulkInfo.groups.length) {
bulkInfo.filePointer++;
if (bulkInfo.filePointer >= bulkInfo.groups[bulkInfo.groupPointer].files.length) {
bulkInfo.filePointer = 0;
bulkInfo.groupPointer++;
}
if (bulkInfo.groupPointer >= bulkInfo.groups.length) {
convertDone();
} else if (!bulkInfo.paused) {
convertNextInBulkQueue();
}
} else {
convertDone();
}
}
}
// jQuery.post(ajaxurl, data, responseCallback);
jQuery.ajax({
method: 'POST',
url: ajaxurl,
data: data,
timeout: 30000, // 30 second timeout per file
success: (response) => {
responseCallback(response);
},
error: (jqXHR, textStatus, errorThrown) => {
var errorMsg = 'unknown error';
if (textStatus === 'timeout') {
errorMsg = 'timeout (30s)';
} else if (textStatus === 'error') {
if (jqXHR.status === 500) {
errorMsg = 'server error (500)';
} else if (jqXHR.status === 0) {
errorMsg = 'connection failed';
} else {
errorMsg = 'error (' + jqXHR.status + ')';
}
} else {
errorMsg = 'error (' + textStatus + ')';
}
// Get current file info for logging
var bulkInfo = window.webpexpress_bulkconvert;
var filename = 'unknown file';
if (bulkInfo && bulkInfo.groups && bulkInfo.groupPointer < bulkInfo.groups.length) {
var group = bulkInfo.groups[bulkInfo.groupPointer];
if (group && group.files && bulkInfo.filePointer < group.files.length) {
filename = group.files[bulkInfo.filePointer];
}
}
logLn('Converting <i>' + filename + '</i> <span style="color:red">' + errorMsg + '</span><br>');
// Continue with next file instead of stopping
if (bulkInfo && bulkInfo.groups && bulkInfo.groupPointer < bulkInfo.groups.length) {
var group = bulkInfo.groups[bulkInfo.groupPointer];
// Get next
bulkInfo.filePointer++;
if (bulkInfo.filePointer >= group.files.length) {
bulkInfo.filePointer = 0;
bulkInfo.groupPointer++;
}
if (bulkInfo.groupPointer >= bulkInfo.groups.length) {
convertDone();
} else {
if (!bulkInfo.paused) {
convertNextInBulkQueue();
}
}
} else {
convertDone();
}
},
});
}