PDF Upload / Download

Charlie Ward
Kilo Expert

I want to upload a PDF somehow to my instance and then call that PDF as a link to be viewed and/or downloaded. I have a UI page setup, but can't figure out how to upload a PDF and then call that PDF in my code. HELP!!!

1 ACCEPTED SOLUTION

Charlie, so I did some looking online and it looks from these strings that truly public kb isn't possible:


Making Knowledge public in V3


making a knowledgebase public


View solution in original post

11 REPLIES 11

Shep Pavlovic
Kilo Explorer

What I know on this so far is, you actually can't do a knowledgebase enabled for public view at all, but you're able to create a preview of the file with the download direct link. As far as I know, it's possible to do so with this PDF editing tool https://w9.pdffiller.com it's more about filling and editing forms, here is an example on how it works with the W-9 tax form, but I see it will fit for your purposes as well

sfarkas
Tera Contributor

A UI Page with the following HTML:

<html>
<head>
    <title>Upload PDF</title>
</head>
<body>
    <h2>Handle PDF Attachments Table</h2>
    <input type="file" id="pdfFile" accept="application/pdf" />
    <button onclick="uploadPDF()">Upload</button>
	<button onclick="downloadPDF()">Download</button>
	<button onclick="canIconnect()">Try it</button>
	<br/>
	<div id="filename"></div>
	<br/>
	<div id="filetype"></div>
	<br/>
    <div id="result"></div>
	<br/>
    <div id="message"></div>

</body>
</html>

The client script piece to upload is:

		function uploadPDF() {
            var fileInput = document.getElementById('pdfFile');
            var file = fileInput.files[0];
            if (!file) {
                alert('Please select a PDF file to upload.');
                return;
            }
			try {
				document.getElementById('filename').innerText = 'PDF file name is ' + file.name;
				
				var reader = new FileReader();
				// The placement of reader.readAsDataURL(file); after reader.onload 
				// in the code is intentional and necessary for the correct functioning 
				// of the FileReader API.

				reader.onload = function(event) {
					//  event.target.result as if you opened the PDF with notepad
					var base64Content = event.target.result.split(',')[1]; // Get Base64 content without the prefix
					var tableName = 'sys_ui_page'; // Set this to the table where the attachment should be uploaded
					var recordSysId = '0e9b89f41b049e1076f215ff034bcb7f'; // Set this to the sys_id of the record

					// Send the raw data (Base64) to the server
					var ga = new GlideAjax('global.AAIfileUploadUtilityGlobal');
					ga.addParam('sysparm_name', 'uploadPDFglobal');

					ga.addParam('tableName', tableName);
					ga.addParam('recordSysId', recordSysId);
					ga.addParam('fileName', file.name);
					document.getElementById('filetype').innerText = 'MIME type is '+file.type;
					ga.addParam('filetype', file.type); 
					ga.addParam('base64Content', base64Content);
					document.getElementById('filename').innerText = 'base64Content is ' + base64Content.slice(0,20);
					ga.getXMLAnswer(_handleUploadResponse);
				};

				// Once the file has been read, the onload event is triggered, and the code 
				// within the onload function executes.

				/*
				When reader.readAsDataURL(file); is called, the FileReader starts reading 
				the file as a Data URL. Once the reading is complete, the onload event is 
				triggered, and the event object inside the onload function contains the 
				Base64-encoded file content, as well as a reference to the original File 
				object that was read.
				*/
				reader.readAsDataURL(file); // Read the file as a Data URL (Base64 encoded)
			}
			catch (error){
				document.getElementById('result').innerText = 'hummph ...' +  error.message;
			}

        }

		function _handleUploadResponse(response)  {
			if(response){
				var result = JSON.parse(response);
				if(result){
					if(result.recordSysId !== '0') {
						document.getElementById('result').innerText = 
							'Attachment uploaded to table: ' + result.tableName +
							'\nRecord sys_id: ' + result.recordSysId +
							'\nAttachment sys_id: ' + result.attachmentSysId;
					} else {
						document.getElementById('result').innerText = 'script include problem: ' + result.tableName;
					}
				} else {
					document.getElementById('result').innerText = 'hummph ... no result' ;
				}
			}
			else {
				document.getElementById('result').innerText = 'hummph ... no response' ;
			}

		}

The client script to download is this:

		function downloadPDF() {
			try{
				var fileInput = document.getElementById('pdfFile');
				var file = fileInput.files[0];
				if (!file) {
					alert('Please select a PDF file to upload.');
					return;
				}
				else {
					alert('You selected this file: ' + file.name);
				}				
				var tableName = 'sys_ui_page'; // Set this to the table where the attachment should be uploaded
				var recordSysId = '0e9b89f41b049e1076f215ff034bcb7f'; // Set this to the sys_id of the record

				var ga = new GlideAjax('global.AAIfileUploadUtilityGlobal');
				ga.addParam('sysparm_name', 'readAttIntoString');
				ga.addParam('tableName', tableName);
				ga.addParam('fileName', file.name);				
				ga.addParam('recordSysId', recordSysId);	
				document.getElementById('result').innerText = 'download ga parms loaded for ' + file.name;
				ga.getXMLAnswer(_handleDownloadResponse);
			}
			catch(error){
				document.getElementById('result').innerText = 'hummph in download ...' +  error.message;
			}
		}

		function convertToCSV(dataAsStringFromScriptInclude) {
			try{
				var itemLen = 4;
				var resultArray = [];
				
				// Loop through the string in steps of itemLen characters
				for (var i = 0; i < dataAsStringFromScriptInclude.length; i += itemLen) {
					// Get the next itemLen characters
					var chunk = dataAsStringFromScriptInclude.substr(i, itemLen);
					resultArray.push(chunk);
				}
				
				// Join the array into a CSV string
				var csvString = resultArray.join(',');
				
				return csvString;
			}
			catch(error){
				return error.message + ' from convertToCSV';
			}
			
		}

		function _handleDownloadResponse(response) {
			if(response !== undefined){
				var dataFromScriptInclude = JSON.parse(response);
				document.getElementById('message').innerText = 'response: ' + dataFromScriptInclude.slice(0,20);
				var dataAsStringFromScriptInclude = dataFromScriptInclude.toString();
				if(dataAsStringFromScriptInclude){
					try {
						// // Ensure the string is correctly padded
						// while (dataAsStringFromScriptInclude.length % 4 !== 0) {
						// 	dataAsStringFromScriptInclude += "=";
						// }
						var stringArray = '';
						var binaryArray = [];
						if (dataAsStringFromScriptInclude.includes(',')) {
			document.getElementById('message').innerText = 'response: ' + dataAsStringFromScriptInclude.slice(0,20) ;
								
						// Step 1: Split the CSV string into an array of strings
							if (dataAsStringFromScriptInclude && typeof dataAsStringFromScriptInclude === 'string') {
								stringArray = dataAsStringFromScriptInclude.split(',');
							} else {
								// Handle the case where dataAsStringFromScriptInclude is not a valid string
								var str = dataAsStringFromScriptInclude.toString();
								document.getElementById('message').innerText = 'str is ' + str.slice(0,20);
								stringArray = str.split(',');
							}

						// Step 2: Convert each string to an integer
							var intArray = stringArray.map(function(num) {
								return parseInt(num, 10);
							});
						
						// Step 3: Create a Uint8Array from the integer array
							binaryArray = new Uint8Array(intArray);
						}
						else {
							document.getElementById('message').innerText = 'response has no commas';
							while (dataAsStringFromScriptInclude.length % 4 !== 0) {
								dataAsStringFromScriptInclude += "=";
							}
							var len = dataAsStringFromScriptInclude.length;
							var binaryData = new Uint8Array(len);
							for (var i = 0; i < len; i++) {
								binaryData[i] = dataAsStringFromScriptInclude.charCodeAt(i);
							}
						}
						var blob = new Blob([binaryArray], { type: 'application/pdf' });	
						// document.getElementById('message').innerText = 'blob is set';

						// Step 4: Create a download link and trigger the download
						var link = document.createElement('a');
						link.href = window.URL.createObjectURL(blob);
						link.download = 'file.pdf';
						link.click();

						// document.getElementById('message').innerText = 'link was clicked';
						// Optionally, revoke the object URL after the download
						window.URL.revokeObjectURL(link.href);	
					}
					catch(error){
					document.getElementById('result').innerText = 'hummph in download handler ...' +  error.message;
					}
			
				} else {
				document.getElementById('result').innerText = 'hummph in download handler  ... no base64String' ;
				}
			}
			else {
			document.getElementById('result').innerText = 'hummph in download handler  ... no download response' ;
			}
		}

 

And the biggest trick is you need a Script Include because of scoping and how it handles Glide objects:

var AAIfileUploadUtilityGlobal = Class.create();
AAIfileUploadUtilityGlobal.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
    // initialize: function() {},
	canIconnect: function() {
		/*
		you may have noticed that we don't return any values from our function. We can if we 
		want, but it is not necessary. The AbstractAjaxProcessor class (which is our 
		superclass - which means we're extending it) has built-in functionality that returns 
		the entire XML document when we're through. 
		*/
		var attrib = this.getParameter('aString');
		var strResponse = 'correct response from AAIfileUploadUtilityGlobal canIconnect ' + attrib;
		return JSON.stringify(strResponse);
		//return strResponse;
	},
	readAttIntoString: function() {
		var tableName = this.getParameter('tableName');
		var recordSysId = this.getParameter('recordSysId'); //sys_id of the record that contains attachment
		var fileName = this.getParameter('fileName');

		var attGr = new GlideRecord('sys_attachment');  //go to sys_attachment table
		attGr.addQuery('table_name', tableName); //specify your table name as second parameter
	attGr.addQuery('table_sys_id', recordSysId); // sys_id of the row associated with the attachment you're trying to get
		attGr.addQuery('file_name', fileName); // full name and extension of the attachment
		attGr.orderByDesc('sys_updated_on'); //helps to retrieve the latest attachment to record
		attGr.query();
		if(attGr.next())
		{
			var gsa = new global.GlideSysAttachment();
			var attachmentData = gsa.getBytes(attGr);
			return JSON.stringify(attachmentData);
		}
	},

    uploadPDFglobal: function() {
		var result = {};
		var step = 'after try start';
		try {
			var tableName = this.getParameter('tableName');
			var recordSysId = this.getParameter('recordSysId');
			var fileName = this.getParameter('fileName');
			var filetype = this.getParameter('filetype');
                       var base64Content = this.getParameter('base64Content'); 
                              // a long string of encoded binary, Base64 content without the prefix

			if(!tableName) {throw new Error('tableName not sent');}
			if(!recordSysId) {throw new Error('recordSysId not sent');}
			if(!fileName) {throw new Error('fileName not sent');}
			if(!filetype) {throw new Error('filetype not sent');}
			if(!base64Content) {throw new Error('base64Content not sent');}
			result = {
				attachmentSysId: 'attachmentSysId',
				tableName: tableName,
				recordSysId: recordSysId			
			};
			if(true){
				step = 'before base64Encode';

				// Base64 content is passed directly; assume it's stored or handled 
				// by another system
				// GlideStringUtil.base64Encode works only in global scope
		var decodedBase64FileConent = GlideStringUtil.base64DecodeAsBytes(base64Content);  // this step is crucial
				step = 'find the UI Page record';
				var rec = new GlideRecord(tableName);
				rec.get(recordSysId);

				step = 'Check if the record was found';
				if (rec.isValidRecord()) {
					var attachment = new global.GlideSysAttachment(); 
					// There is a scoping error in this version of SN where the GlideSysAttachment API.
			// writeBase64(GlideRecord now_GR, String fileName, String contentType, String content_base64Encoded)
					step = 'try to attach with writeBase64';
					var attachmentSysId = attachment.write(rec, fileName, filetype, decodedBase64FileConent); 
			// writeBase64 is only for scoped applications
			// unfortunately, GlideStringUtil is only available in the global scope, so I made this script include global
					step = 'after writeBase64';
					if(attachmentSysId){
						result = {
								attachmentSysId: attachmentSysId,
								tableName: tableName,
								recordSysId: recordSysId
						};
					}
					else {
						result = {
								attachmentSysId: '-1212',
								tableName: 'attachmentSysId has no value',
								recordSysId: '0'
						};					
					}
	
				} else {
					result = {
						attachmentSysId: '-2758',
						tableName: 'invalid sys_id for tablename of ' + tableName,
						recordSysId: recordSysId
					};
				}
			}
		} catch (error) {
			result = {
				attachmentSysId: '0',
				tableName: 'Error: ' + error.message + ' at ' + step,
				recordSysId: '0'
			};
		}
        // Return the result as a JSON string
        return JSON.stringify(result);
	},

    type: 'AAIfileUploadUtilityGlobal'
});