Download embedded PDF loaded in WKWebView - javascript

While loading HTML5 page from url I'm getting pdf somewhere in that page and I have to download that pdf or save it as base64.
This is where the pdf is in the HTML code. I cannot simply hit the 'src' URL and get the pdf.
< embed width="100%" height="100%" name="plugin" id="plugin" src="https://myurl.com/fileToOpen.pdf” type="application/pdf" internalinstanceid="8" title="">
Any JS which can help me get base64 string of that or any other method to download?

This question asked sometimes back,
But if someone looking for swift solution with WKWebView to download .pdf or any file on File manager, this is how I ended-up
class WebPortalVC: UIViewController, WKNavigationDelegate,WKUIDelegate, UIDocumentInteractionControllerDelegate,URLSessionDownloadDelegate {
override following function, that will intercept url, in our case we check ulr ending with .pdf and .csv and redirect to open with file manger view. which enable view file, download and save on device storage, airdrop or share with other apps
just add following functions and check.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url {
print("fileDownload: check :: \(url)")
let extention = "\(url)".suffix(4)
if extention == ".pdf" || extention == ".csv"{
print("fileDownload: redirect to download events. \(extention)")
DispatchQueue.main.async {
self.downloadPDF(tempUrl: "\(url)")
}
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}
func downloadPDF(tempUrl:String){
print("fileDownload: downloadPDF")
guard let url = URL(string: tempUrl) else { return }
let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
//showHUD(isShowBackground: true); //show progress if you need
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
print("fileDownload: documentInteractionControllerViewControllerForPreview")
return self
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// create destination URL with the original pdf name
print("fileDownload: urlSession")
guard let url = downloadTask.originalRequest?.url else { return }
print("fileDownload: urlSession \(url)")
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsPath.appendingPathComponent(url.lastPathComponent)
// delete original copy
try? FileManager.default.removeItem(at: destinationURL)
// copy from temp to Document
do {
try FileManager.default.copyItem(at: location, to: destinationURL)
myViewDocumentsmethod(PdfUrl:destinationURL)
print("fileDownload: downloadLocation", destinationURL)
DispatchQueue.main.async {
NBMaterialToast.showWithText(self.view, text: "Download Completed", duration: NBLunchDuration.long)
}
} catch let error {
print("fileDownload: error \(error.localizedDescription)")
}
// dismissHUD(isAnimated: false); //dismiss progress
}
func myViewDocumentsmethod(PdfUrl:URL){
print("fileDownload: myViewDocumentsmethod \(PdfUrl)")
DispatchQueue.main.async {
let controladorDoc = UIDocumentInteractionController(url: PdfUrl)
controladorDoc.delegate = self
controladorDoc.presentPreview(animated: true)
}
}

Update
From the Docs they say
The Fetch API provides an interface for fetching resources (including
across the network). It will seem familiar to anyone who has used
XMLHttpRequest
You can also use the following string to get the base64 String from the WKWebview
let s = "path = document.getElementById(\"plugin\").src\n" +
"\n" +
"fetch(path).then(function (response) {\n" +
" response.body.getReader().read().then(function(result) {\n" +
" return btoa(String.fromCharCode.apply(null, result.value));\n" +
" }).then(function(b64) {\n" +
" window.webkit.messageHandlers.myInterface.postMessage(b64);\n" +
" });\n" +
"});"
both fetch and xmlhttp works async.. all you need to do is wait when the processing gets completed pass it to the Swift using javascript's bridge to ios (WKScriptMessageHandler)
Use the following code to get the base64 string from javascript to Swift.
I am using WKScriptMessageHandler to get the callback from Javascript when the base64 string is ready to be consumed. In the String s you just need to pass the url of the pdf and it will do a ajax request to get the pdf file and then convert it to base64 string.
import UIKit
import WebKit
class ViewController: UIViewController {
#IBOutlet weak var btnPDF: UIButton!
#IBOutlet weak var webViewParentView: UIView!
var activityIndicator: UIActivityIndicatorView?
var webView: WKWebView!
#objc func didSelect(_ sender: UIView){
let s="var xhr = new XMLHttpRequest();\n" +
"xhr.open(\'GET\', \"https://codingexceptions.com/wkwebview/dummy.pdf\", true);\n" +
"\n" +
"xhr.responseType = \'arraybuffer\';\n" +
"\n" +
"xhr.onload = function(e) {\n" +
" if (this.status == 200) {\n" +
" var uInt8Array = new Uint8Array(this.response);\n" +
" var i = uInt8Array.length;\n" +
" var binaryString = new Array(i);\n" +
" while (i--)\n" +
" {\n" +
" binaryString[i] = String.fromCharCode(uInt8Array[i]);\n" +
" }\n" +
" var data = binaryString.join(\'\');\n" +
"\n" +
" var base64 = window.btoa(data);\n" +
"\n" +
"window.webkit.messageHandlers.myInterface.postMessage(base64);" +
"\n" +
" }\n" +
"};\n" +
"\n" +
"xhr.send();\n"
webView.configuration.userContentController.add(self, name: "myInterface")
webView?.evaluateJavaScript(s, completionHandler: {(string,error) in
print(error ?? "no error")
})
}
func setupWebView(){
webView = WKWebView.init(frame: CGRect(x: 0, y: 0, width: webViewParentView.frame.width, height: webViewParentView.frame.height))
webView.navigationDelegate = self
webViewParentView.addSubview(webView)
activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
activityIndicator?.center = self.view.center
self.view.addSubview(activityIndicator!)
webView.load(URLRequest(url: URL(string: "https://codingexceptions.com/wkwebview/index.php")!))
activityIndicator?.startAnimating()
}
override func viewDidLoad() {
super.viewDidLoad()
btnPDF.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setupWebView()
}
}
extension ViewController: WKScriptMessageHandler{
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("Message received: \(message.name) with body: \(message.body)")
}
}
extension ViewController: WKNavigationDelegate{
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.activityIndicator?.stopAnimating()
self.activityIndicator?.removeFromSuperview()
self.activityIndicator = nil
}
}
Update: To get the source from the embed tag as pointed in #Tarun's answer
just put the below line in the starting of string variable s and pass the url in xhr.open
var url = document.getElementById("plugin").src

PS: Using answer as comments, as I need formatting
You should execute the below JavaScript in the webview
path = document.getElementById("plugin").src
fetch(path).then(function (response) {
response.body.getReader().read().then(function(result) {
return btoa(String.fromCharCode.apply(null, result.value));
}).then(function(b64) {
window.pdf_data = b64;
});
});
Then you can execute another query to access the window.pdf_data assuming get return value from a javascript execution is possible?

Are you looking to download the PDF to your iPhone or to your Mac? Generally, it's not possible to download PDFs directly to your iPhone because the iPhone doesn't natively have the capability to store PDFs. You'd need to have iBooks or iCloud Drive and then open the PDF in another window, followed by manually downloading it. There would still need to be user-interaction before you download the PDF, meaning the user would have to approve the download. Direct download via the injection of JavaScript into a WKWebView instance is is not possible.

Related

How to download the PDF in Jquery ajax call using java

I created a service to download a PDF file.
On my server-side(Java) the PDF is generated successfully. But I am unable to download that on the UI side (Using Jquery Ajax call).
Could anyone please help me with this?
$(document).on('click', '.orderView', function(event){
orderId = $(this).attr('data');
$.ajax({
type : 'GET',
contentType : 'application/json',
url : '../service/purchase/generateInventoryPurchasePdf/'+orderId,
success : function(response) {
console.log("Success");
},
error : function(response) {
console.log("Error :" + response);
}
});
});
Java Code:
#RequestMapping(value = "/generateInventoryPurchasePdf/{purchaseId}", method = RequestMethod.GET)
public ResponseEntity<ByteArrayResource> generateInventoryPurchasePdf(HttpServletResponse response,#PathVariable("purchaseId") Long purchaseId) throws Exception {
PurchaseOrder purchaseOrder = null;
purchaseOrder = purchaseService.findByPurchaseOrderId(purchaseId);
// generate the PDF
Map<Object,Object> pdfMap = new HashMap<>();
pdfMap.put("purchaseOrder", purchaseOrder);
pdfMap.put("purchaseOrderDetail", purchaseOrder.getPurchaseOrderDetail());
pdfMap.put("vendorName", purchaseOrder.getInvVendor().getName());
pdfMap.put("vendorAddrs", purchaseOrder.getInvVendor().getVenAddress().get(0));
File file = util.generatePdf("email/purchasepdf", pdfMap);
MediaType mediaType = MediaTypeUtils.getMediaTypeForFileName(this.servletContext, file.getName());
System.out.println("fileName: " + file.getName());
System.out.println("mediaType: " + mediaType);
//Path path = Paths.get(file.getAbsolutePath() + "/" + file.getName());
Path path = Paths.get(file.getAbsolutePath());
byte[] data = Files.readAllBytes(path);
ByteArrayResource resource = new ByteArrayResource(data);
return ResponseEntity.ok()
// Content-Disposition
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + path.getFileName().toString())
// Content-Type
.contentType(mediaType) //
// Content-Lengh
.contentLength(data.length) //
.body(resource);
}
mediaUtil class:
public class MediaTypeUtils {
public static MediaType getMediaTypeForFileName(ServletContext servletContext, String fileName) {
// application/pdf
// application/xml
// image/gif, ...
String mineType = servletContext.getMimeType(fileName);
try {
MediaType mediaType = MediaType.parseMediaType(mineType);
return mediaType;
} catch (Exception e) {
return MediaType.APPLICATION_OCTET_STREAM;
}
}
}
PDF Generation code:
public File generatePdf(String templateName, Map<Object, Object> map) throws Exception {
Assert.notNull(templateName, "The templateName can not be null");
Context ctx = new Context();
if (map != null) {
Iterator<Entry<Object, Object>> itMap = map.entrySet().iterator();
while (itMap.hasNext()) {
Map.Entry<Object, Object> pair = itMap.next();
ctx.setVariable(pair.getKey().toString(), pair.getValue());
}
}
String processedHtml = templateEngine.process(templateName, ctx);
FileOutputStream os = null;
String fileName = "POLIST";
try {
final File outputFile = File.createTempFile(fileName, ".pdf",new File(servletContext.getRealPath("/")));
outputFile.mkdir();
os = new FileOutputStream(outputFile);
ITextRenderer renderer = new ITextRenderer();
renderer.setDocumentFromString(processedHtml);
renderer.layout();
renderer.createPDF(os, false);
renderer.finishPDF();
System.out.println("PDF created successfully");
return outputFile;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
}
}
}
}
I'm not getting any error, PDF generate successfully in the server side. But In UI side not working.
Downloading files via AJAX isn't really a logical thing to do. When you make an AJAX call, the data returned from the server is returned into your page's JavaScript code (in the response callback value), rather than being returned to the browser itself to decide what to do. Therefore the browser has no way to initiate a download, because the browser is not directly in control of the response - your JavaScript code is in control instead.
As you've indicated in your comment below the question, there are workarounds you can use, but really the best approach is simply to use a regular non-AJAX request to download
For instance you could replace your jQuery code with something like
$(document).on('click', '.orderView', function(event){
orderId = $(this).attr('data');
window.open('../service/purchase/generateInventoryPurchasePdf/'+orderId);
});
This will download the document from a new tab without navigating away from the current page.

iPhone swift app development using WKWebview - How to trigger a local notification immediately when a javascript event happens

I am writing an iPhone app in swift using WKWebView that loads a web page, and I need to trigger a local notification once a particular javascript even happens (once I see this "danger-badge" image on the page, which pops up via javascript). Currently I am able to "schedule" a local notification once I see this javascript event (see code below), but I need to trigger the local notification immediately after the javascript event, whenever it happens. Currently I am only able to schedule a reoccurring local notification.
I am not doing push notifications because the app is just a basic browser that points to a website that has javascript events on it, so basically I want the app to mirror the notification that happens on the web page that's loaded.
Here is my swift code using the WKWebView (pardon the comments. I've been playing with this for a while):
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler {
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
let contentController = WKUserContentController()
let js: String = " var parentNodes = document.getElementsByClassName('badge badge-danger'); var text; text = 'Nodename: ' + parentNodes[0].nodeName + ' Classname: ' + parentNodes[0].className + ' ' + parentNodes[0].textContent + ' \\n'; window.webkit.messageHandlers.callbackHandler.postMessage(text); "
let userScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: false)
contentController.removeAllUserScripts()
contentController.addUserScript(userScript)
// Add ScriptMessageHandler
contentController.add(
self,
name: "callbackHandler"
)
webConfiguration.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
webView.navigationDelegate = self
webView.allowsBackForwardNavigationGestures = true
view = webView
let myURL = URL(string: "https://websiteurl.com/dev")
let myRequest = URLRequest(url: myURL!)
webView.load(myRequest)
}
override func viewDidLoad() {
super.viewDidLoad()
//...
}
// Implement `WKScriptMessageHandler`,handle message which been sent by JavaScript
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
//if(message.name == "callbackHandler") {
let messageBody = "\(message.body)"
if #available(iOS 10, *)
{
//UIApplication.shared.applicationIconBadgeNumber = 0
//let center = UNUserNotificationCenter.current()
//center.removeAllDeliveredNotifications() // To remove all delivered notifications
//center.removeAllPendingNotificationRequests() // To remove all pending notifications which are not delivered yet but scheduled.*/
if messageBody.contains("badge badge-danger")
{
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { (didAllow, error) in})
let content = UNMutableNotificationContent()
content.title = "You have a new notification!"
//content.badge = 1
content.body = "new notification..."
let date = Date()
var triggerNow = Calendar.current.dateComponents([.hour,.minute,.second], from:date)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
print(trigger.nextTriggerDate())
let request = UNNotificationRequest(identifier: content.title, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: {(error) in
print(error)
})
print("JavaScript message: TEST \(message.body)")
}
}
}
}

InDesign ExtendScript script sometimes creates a corrupted PDF during export

I've had this problem for a while, now. Close to the end of my "Proofing" script, the currently opened document in InDesign is to be exported to two different .pdf files. The first is password-protected while the second is not. I don't seem to have any problems with the latter, but the former often becomes corrupted somehow and cannot be opened by any PDF reader, including Acrobat itself. Here's the code block that does the exporting (it is not runnable by itself, btw):
/********** BEGIN PDF EXPORTING **********/
// First, let's create and set PDF export preferences.
// This begins with creating a temporary preset if it doesn't already exist.
// This preset will be used for both the Proof page and the Cover sheet.
var tempPreset = app.pdfExportPresets.item("tempPreset");
try
{
tempPreset.name;
}
catch (eNoSuchPreset)
{
tempPreset = app.pdfExportPresets.add({name:"tempPreset"});
}
with (tempPreset)
{
acrobatCompatibility = AcrobatCompatibility.ACROBAT_5;
bleedMarks = false;
colorBars = false;
colorBitmapCompression = BitmapCompression.AUTO_COMPRESSION;
colorBitmapQuality = CompressionQuality.MAXIMUM;
colorBitmapSampling = Sampling.BICUBIC_DOWNSAMPLE;
colorBitmapSamplingDPI = 300;
compressTextAndLineArt = true;
cropImagesToFrames = true;
cropMarks = false;
exportGuidesAndGrids = false;
exportNonprintingObjects = false;
exportReaderSpreads = false;
exportWhichLayers = ExportLayerOptions.EXPORT_VISIBLE_PRINTABLE_LAYERS;
generateThumbnails = false;
grayscaleBitmapCompression = BitmapCompression.AUTO_COMPRESSION;
grayscaleBitmapQuality = CompressionQuality.MAXIMUM;
grayscaleBitmapSampling = Sampling.BICUBIC_DOWNSAMPLE;
grayscaleBitmapSamplingDPI = 300;
includeBookmarks = false;
includeHyperlinks = false;
includeSlugArea = false;
includeStructure = true;
monochromeBitmapCompression = MonoBitmapCompression.CCIT4;
monochromeBitmapSampling = Sampling.BICUBIC_DOWNSAMPLE;
monochromeBitmapSamplingDPI = 1200;
omitBitmaps = false;
omitEPS = false;
omitPDF = false;
optimizePDF = true;
pageInformationMarks = false;
pageMarksOffset = 0.0833;
pdfMarkType = MarkTypes.DEFAULT_VALUE;
printerMarkWeight = PDFMarkWeight.P25PT;
registrationMarks = false;
standardsCompliance = PDFXStandards.NONE;
subsetFontsBelow = 100;
thresholdToCompressColor = 450;
thresholdToCompressGray = 450;
thresholdToCompressMonochrome = 1800;
useDocumentBleedWithPDF = false;
}
currentProcess.text = "PDF export preferences"; progressWin.show();
progressIndividual.value++; if (aProducts.length > 1) {progressOverall.value++;}
// Now let's actually set the export preferences. These are for the proof page.
with (app.pdfExportPreferences)
{
pageRange = proofRange;
useSecurity = true;
disallowChanging = true;
disallowCopying = false;
disallowDocumentAssembly = true;
disallowExtractionForAccessibility = false;
disallowFormFillIn = true;
disallowHiResPrinting = true;
disallowNotes = true;
disallowPlaintextMetadata = true;
disallowPrinting = false;
changeSecurityPassword = "sky";
if (multiColor)
{
pageRange = colorTable.toString();
}
if (currentProduct.pLabel != "")
{
pageRange += "," + labelPage.name;
}
}
currentProcess.text = "Exporting PDF proof page"; progressWin.show();
progressIndividual.value++; if (aProducts.length > 1) {progressOverall.value++;}
// Before exporting the Proof page(s), hide the color bar on multicolor products.
if (multiColor) {document.layers.item("COLOR BAR").visible = false;}
// Then we save the proof page.
document.exportFile(ExportFormat.PDF_TYPE, File(jobFolder.toString() + "/" + saveName + ".pdf"), false, tempPreset);
When that produced corrupted PDFs once in a while, I thought that perhaps it was our less-than-ideal network structure causing the problem, so I instead tried exporting the PDF file to the local hard drive rather than directly to the network, then having the file be moved to the network afterward. So, the last line in the above code block was replaced with:
// First, to the local HDD.
document.exportFile(ExportFormat.PDF_TYPE, File("~/Documents/" + saveName + ".pdf"), false, tempPreset);
$.sleep(1000);
File("~/Documents/" + saveName + ".pdf").copy(File(jobFolder.toString() + "/" + saveName + ".pdf"));
$.sleep(1000);
File("~/Documents/" + saveName + ".pdf").remove();
I even added in those 1-second delays, just in case. Sadly, this hasn't helped. I am still getting a corrupted PDF every now and then. If there is any pattern to the corrupted files, I haven't been able to discern it. Does anyone have any thoughts?
It finally hit me that, if the corrupted files are not able to be opened in Acrobat, then why not just test for that after the file is created? So I created a loop that exports the PDF file and tries to open it in Acrobat. If it opens fine, then it prints and closes the file, returning a "true" message. If it is unable to do so, then it returns a "false" message to the script. Then the loop repeats so long as that message is "false". While not a great fix for the underlying cause (whatever it may be), it at least is a workaround that will do just fine for our needs. The trick is that, because we work with Macs, we have to route the message through an AppleScript instead of using BridgeTalk to communicate directly with Acrobat.
Here's the code snippet from the main InDesign script which goes through the PDF-checking loop:
// Then we save the proof page.
// The loop is to make sure that the file was saved properly.
var validFile = false; // Flag that states whether or not the file is corrupted after saving.
var rString; // String returned from Acrobat that should be either "true" or "false".
var testAndPrintFile = File("~/Documents/testAndPrint.applescript"); // The applescript file that calls Acrobat and runs a folder-level script.
var pdfFile; // A String of the filename & path that will be passed to through the applescript file to Acrobat.
var pdfArray = new Array(4); // An array to send to Acrobat. [0] is the PDF filename as a String,
// [1] is duplex if true, [2] is the printer name, and [3] is to enable printing.
if (multiTwoSided || twoPages) pdfArray[1] = "true";
else pdfArray[1] = "false";
pdfArray[2] = localPrinter;
pdfArray[3] = "true";
while (!validFile)
{
$.writeln("If this message is seen more than once, then the Proof PDF was corrupted.");
try
{
document.exportFile(ExportFormat.PDF_TYPE, File(jobFolder.toString() + "/" + saveName + ".pdf"), false, tempPreset);
}
catch (e)
{
alert("Could not save the Proof PDF. Please close any open copies of the Proof PDF, then save and print it manually.");
}
pdfFile = jobFolder.toString() + "/" + saveName + ".pdf";
pdfArray[0] = pdfFile;
$.writeln("pdfArray contains: " + pdfArray);
try
{
rString = app.doScript(testAndPrintFile, ScriptLanguage.APPLESCRIPT_LANGUAGE, pdfArray);
validFile = rString == "true";
// validFile = true;
$.writeln("validFile is " + validFile);
if (!validFile)
{
alert("It seems that the file " + unescape(pdfArray[0]) + " is corrupted. Will try to export it again.");
}
}
catch (e)
{
$.writeln("ERROR at line number " + e.line);
$.writeln(e.description);
throw new Error("ERROR at line number " + e.line + "\n" + e.description);
}
}
The testAndPrint.applescript file that this loop calls:
set pdfFile to item 1 of arguments
set duplexed to item 2 of arguments
set printerName to item 3 of arguments
set printEnabled to item 4 of arguments
tell application "Adobe Acrobat Pro"
set result to do script ("testAndPrint(\"" & pdfFile & "\", \"" & duplexed & "\", \"" & printerName & "\", \"" & printEnabled & "\");")
end tell
return result
And, finally, the folder-level Javascript file that is loaded into memory when Acrobat starts, ready to have its function called by the above Applescript file:
var testAndPrint = app.trustedFunction(function (fName, duplexed, sPrinterName, bEnablePrinting)
{
var success = true;
app.beginPriv();
console.println("fName is " + unescape(fName));
console.println("sPrinterName is " + sPrinterName);
try
{
var printDoc = app.openDoc(unescape(fName));
var pp = printDoc.getPrintParams();
if (duplexed == "true") pp.DuplexType = pp.constants.duplexTypes.DuplexFlipLongEdge;
else pp.DuplexType = pp.constants.duplexTypes.Simplex;
pp.printerName = sPrinterName;
pp.interactive = pp.constants.interactionLevel.silent;
pp.pageHandling = pp.constants.handling.none;
if (bEnablePrinting == "true") printDoc.print({bUI: false, bSilent: true, bShrinkToFit: false, printParams: pp});
printDoc.closeDoc(true);
}
catch (e)
{
console.println("ERROR at line number " + e.lineNumber);
console.println(e.message);
success = false;
}
app.endPriv();
console.println("success is " + success);
return success;
});
I hope that, perhaps, this information might be useful to anyone else running into a similar problem. It's not pretty, of course, but it certainly gets the job done.

Capturing console.log within a UIWebView

I'm writing an iOS application with MonoTouch that does some javascript interaction with a UIWebView. For debugging purposes, it would be nice to be able to "capture" console.log in the javascript that runs in the UIWebView together with the rest of the application output. Is this possible? Examples using regular Objective-C code is also OK.
After some more googling, I came about this answer: Javascript console.log() in an iOS UIWebView
Converting it to MonoTouch yields this solution:
using System;
using System.Web;
using System.Json;
using MonoTouch.UIKit;
namespace View
{
public class JsBridgeWebView : UIWebView
{
public object BridgeDelegate {get;set;}
private const string BRIDGE_JS = #"
function invokeNative(functionName, args) {
var iframe = document.createElement('IFRAME');
iframe.setAttribute('src', 'jsbridge://' + functionName + '#' + JSON.stringify(args));
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
var console = {
log: function(msg) {
invokeNative('Log', [msg]);
}
};
";
public JsBridgeWebView ()
{
ShouldStartLoad += LoadHandler;
LoadFinished += (sender, e) => {
EvaluateJavascript(BRIDGE_JS);
};
}
public bool LoadHandler (UIWebView webView, MonoTouch.Foundation.NSUrlRequest request, UIWebViewNavigationType navigationType)
{
var url = request.Url;
if(url.Scheme.Equals("jsbridge")) {
var func = url.Host;
if(func.Equals("Log")) {
// console.log
var args = JsonObject.Parse(HttpUtility.UrlDecode(url.Fragment));
var msg = (string)args[0];
Console.WriteLine(msg);
return false;
}
return true;
}
}
}
}
Now all console.log statements in javascript in a UIWebView will be sent to Console.WriteLine. This could of course be extended to any kind of output one would want.
Can you add javascript code that does something like this to overwrite the method:
console.log = function(var text) {
consoleforios += text;
}
Then from the web view, call:
string console = webView.EvaluatingJavaScript("return consoleforios;");
This might not be something I'd leave in permanently, but it should work.

Server Side Logging Of Client Side Javascript Crashes

I have a large complex web app with thousands of lines of Javascript. There is a small set of intermittent Javascript bugs that are report by users.
I think these are epiphenomena of race conditions - something has not initialised correctly and the Javascript crashes causing 'down stream' js not to run.
Is there anyway to get Javascript execution crashes to log back server side?
All the js logging libraries like Blackbird and Log4JavaScript are client-side only.
I have written a remote error logging function using window.onerror as suggested by #pimvdb
Err = {};
Err.Remoterr = {};
Err.Remoterr.onerror = function (msg, errorfileurl, lineno) {
var jsonstring, response, pageurl, cookies;
// Get some user input
response = prompt("There has been an error. " +
"It has been logged and will be investigated.",
"Put in comments (and e-mail or phone number for" +
" response.)");
// get some context of where and how the error occured
// to make debugging easier
pageurl = window.location.href;
cookies = document.cookie;
// Make the json message we are going to post
// Could use JSON.stringify() here if you are sure that
// JSON will have run when the error occurs
// http://www.JSON.org/js.html
jsonstring = "{\"set\": {\"jserr\": " +
"{\"msg\": \"" + msg + "\", " +
"\"errorfileurl\": \"" + errorfileurl + "\", " +
"\"pageurl\": \"" + pageurl + "\", " +
"\"cookies\": \"" + cookies + "\", " +
"\"lineno\": \"" + lineno + "\", " +
"\"response\": \"" + response + "\"}}}";
// Use the jquery cross-browser post
// http://api.jquery.com/jQuery.post/
// this assumes that no errors happen before jquery has initialised
$.post("?jserr", jsonstring, null, "json");
// I don't want the page to 'pretend' to work
// so I am going to return 'false' here
// Returning 'true' will clear the error in the browser
return false;
};
window.onerror = Err.Remoterr.onerror;
I deploy this between the head and body tags of the webpage.
You will want to change the JSON and the URL that you post it to depending on how you are going to log the data server side.
Take a look at https://log4sure.com (disclosure: I created it) - but it is really useful, check it out and decide for yourself. It allows you to log errors/event and also lets you create your custom log table. It also allows you to monitor your logs real-time. And the best part, its free.
You can also use bower to install it, use bower install log4sure
The set up code is really easy too:
// setup
var _logServer;
(function() {
var ls = document.createElement('script');
ls.type = 'text/javascript';
ls.async = true;
ls.src = 'https://log4sure.com/ScriptsExt/log4sure.min.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ls, s);
ls.onload = function() {
// use your token here.
_logServer = new LogServer("use-your-token-here");
};
})();
// example for logging text
_logServer.logText("your log message goes here.")
//example for logging error
divide = function(numerator, divisor) {
try {
if (parseFloat(value) && parseFloat(divisor)) {
throw new TypeError("Invalid input", "myfile.js", 12, {
value: value,
divisor: divisor
});
} else {
if (divisor == 0) {
throw new RangeError("Divide by 0", "myfile.js", 15, {
value: value,
divisor: divisor
});
}
}
} catch (e) {
_logServer.logError(e.name, e.message, e.stack);
}
}
// another use of logError in window.onerror
// must be careful with window.onerror as you might be overwriting some one else's window.onerror functionality
// also someone else can overwrite window.onerror.
window.onerror = function(msg, url, line, column, err) {
// may want to check if url belongs to your javascript file
var data = {
url: url,
line: line,
column: column,
}
_logServer.logError(err.name, err.message, err.stack, data);
};
// example for custom logs
var foo = "some variable value";
var bar = "another variable value";
var flag = "false";
var temp = "yet another variable value";
_logServer.log(foo, bar, flag, temp);

Categories

Resources