package org.commcare.models; import android.content.Context; import android.content.Intent; import android.util.Pair; import org.commcare.CommCareApplication; import org.commcare.android.logging.ForceCloseLogger; import org.commcare.core.process.XmlFormRecordProcessor; import org.commcare.data.xml.TransactionParser; import org.commcare.engine.cases.CaseUtils; import org.commcare.models.database.SqlStorage; import org.commcare.android.database.user.models.FormRecord; import org.commcare.preferences.DeveloperPreferences; import org.commcare.utils.FormUploadUtil; import org.commcare.xml.AndroidTransactionParserFactory; import org.commcare.xml.LedgerXmlParsers; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.kxml2.io.KXmlParser; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Date; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.spec.SecretKeySpec; /** * A FormRecordProcessor keeps track of all of the logic needed to process * forms and keep track of models/changes. * * TODO: We should move most of the "cleanup" task methods here. * * @author ctsims */ public class FormRecordProcessor { private final Context c; private final SqlStorage<FormRecord> storage; private boolean isBulkProcessing = false; private boolean isPurgePending = false; public FormRecordProcessor(Context c) { this.c = c; storage = CommCareApplication.instance().getUserStorage(FormRecord.class); } /** * This is the entry point for processing a form. New transaction types * should all be declared here. */ public FormRecord process(FormRecord record) throws InvalidStructureException, IOException, XmlPullParserException, UnfullfilledRequirementsException { String form = record.getPath(c); final File f = new File(form); final Cipher decrypter = FormUploadUtil.getDecryptCipher((new SecretKeySpec(record.getAesKey(), "AES"))); InputStream is = new CipherInputStream(new FileInputStream(f), decrypter); AndroidTransactionParserFactory factory = new AndroidTransactionParserFactory(c, null) { @Override public TransactionParser getParser(KXmlParser parser) { String namespace = parser.getNamespace(); String name = parser.getName(); if (LedgerXmlParsers.STOCK_XML_NAMESPACE.equals(namespace) || "case".equalsIgnoreCase(name)) { return super.getParser(parser); } else { return null; } } }; XmlFormRecordProcessor.process(is, factory); //Let anyone who is listening know! Intent i = new Intent("org.commcare.dalvik.api.action.data.update"); i.putStringArrayListExtra("cases", factory.getCreatedAndUpdatedCases()); c.sendBroadcast(i, "org.commcare.dalvik.provider.cases.read"); //Update the record before trying to purge, so we don't block on this, in case //anything weird happens. We don't want to get into a loop FormRecord updatedRecord = updateRecordStatus(record, FormRecord.STATUS_UNSENT); if(factory.wereCaseIndexesDisrupted()) { if(isBulkProcessing) { isPurgePending = true; } else { performPurge(); } } return updatedRecord; } public FormRecord updateRecordStatus(FormRecord record, String newStatus) { // update the records to show that the form has been processed and is // ready to be sent; record = record.updateInstanceAndStatus(record.getInstanceURI().toString(), newStatus); storage.write(record); return record; } public FormRecord getRecord(int dbId) { //this seems silly. return storage.read(dbId); } private void performPurge() { if(DeveloperPreferences.isAutoPurgeEnabled()) { CaseUtils.purgeCases(); } } public void beginBulkSubmit() { isBulkProcessing = true; isPurgePending = false; } public void closeBulkSubmit() { isBulkProcessing = false; if(isPurgePending) { performPurge(); } isPurgePending = false; } /** * Performs deep checks on the current form data to establish whether or * not the files are in a consistent state. Returns a (human readable) * report if not to aid in debugging * * @param r A Form Record to process * @return A tuple whose first argument is a boolean specifying whether the * record has passed the verification process. The second argument is a * human readable report for debugging. */ public Pair<Boolean, String> verifyFormRecordIntegrity(FormRecord r) { StringBuilder reporter = new StringBuilder(); try { reporter.append("\n").append(r.toString()).append("\n"); String formPath; try { //make sure we can retrieve a record. formPath = r.getPath(c); } catch (FileNotFoundException e) { e.printStackTrace(); reporter.append("ERROR - No file path found for form record. ").append(e.getMessage()).append("\n"); return new Pair<>(false, reporter.toString()); } //now, make sure there's a file there File recordFile = new File(formPath); if (!recordFile.exists()) { reporter.append("ERROR - No form at file path provided\n"); return new Pair<>(false, reporter.toString()); } //Give us the info about all of the files in this instance reporter.append("\n-File Report-\n"); File folder = recordFile.getParentFile(); for (File f : folder.listFiles()) { reporter.append(String.format("File:%s \n[Size:%s]\n[LastTouched:%s]\n", f.getName(), String.valueOf(f.length()), new Date(f.lastModified()).toString())); } reporter.append("\n-Instance Report-\n"); reporter.append(String.format("Size on Disk:%s\n", String.valueOf(recordFile.length()))); if (!performLinearFileScan(r, recordFile, false, reporter, "Encrypted Instance File")) { return new Pair(false, reporter.toString()); } if (!performLinearFileScan(r, recordFile, true, reporter, "Decrypted Instance File")) { return new Pair(false, reporter.toString()); } if (!attemptXmlScan(r, recordFile, reporter)) { return new Pair(false, reporter.toString()); } return new Pair(true, reporter.toString()); } catch (Exception e) { return new Pair(false, "Error while preparing attached integrity report: " + e.getMessage() + "\n" + reporter.toString()); } } private boolean performLinearFileScan(FormRecord r, File recordFile, boolean useCipher, StringBuilder reporter, String label) { //Try to read the actual bytes inline InputStream is = null; byte[] buffer = new byte[512]; try { //decrypter if (useCipher) { Cipher decrypter = FormUploadUtil.getDecryptCipher((new SecretKeySpec(r.getAesKey(), "AES"))); is = new CipherInputStream(new FileInputStream(recordFile), decrypter); } else { is = new FileInputStream(recordFile); } long accumulated = 0; int read = 0; while (read != -1) { accumulated += read; read = is.read(buffer); } reporter.append("PASS: Linear scan of ").append(label).append(". ").append(accumulated).append(" bytes read in total\n"); return true; } catch (Exception e) { reporter.append("FAILURE: Error during linear scan of ").append(label).append("\n").append(ForceCloseLogger.getStackTrace(e)); return false; } finally { try { if (is != null) { is.close(); } } catch (IOException ioe) { } } } private boolean attemptXmlScan(FormRecord r, File recordFile, StringBuilder reporter) { KXmlParser parser = new KXmlParser(); InputStream is = null; try { Cipher decrypter = FormUploadUtil.getDecryptCipher((new SecretKeySpec(r.getAesKey(), "AES"))); is = new CipherInputStream(new FileInputStream(recordFile), decrypter); parser.setInput(is, "UTF-8"); parser.setFeature(KXmlParser.FEATURE_PROCESS_NAMESPACES, true); while (parser.next() != KXmlParser.END_DOCUMENT) { //nothing } reporter.append("PASS: Instance file reads as valid XML\n"); return true; } catch (Exception e) { reporter.append("FAILURE: XML Instance file could not be validated\n").append(ForceCloseLogger.getStackTrace(e)); return false; } finally { try { if (is != null) { is.close(); } } catch (IOException ioe) { } } } }