package org.xmlrpc.android; import android.text.TextUtils; import android.util.Base64; import android.util.Xml; import org.wordpress.android.util.helpers.MediaFile; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.StringUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.SimpleTimeZone; class XMLRPCSerializer { static final String TAG_NAME = "name"; static final String TAG_MEMBER = "member"; static final String TAG_VALUE = "value"; static final String TAG_DATA = "data"; static final String TYPE_INT = "int"; static final String TYPE_I4 = "i4"; static final String TYPE_I8 = "i8"; static final String TYPE_DOUBLE = "double"; static final String TYPE_BOOLEAN = "boolean"; static final String TYPE_STRING = "string"; static final String TYPE_DATE_TIME_ISO8601 = "dateTime.iso8601"; static final String TYPE_BASE64 = "base64"; static final String TYPE_ARRAY = "array"; static final String TYPE_STRUCT = "struct"; static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss", Locale.US); static Calendar cal = Calendar.getInstance(new SimpleTimeZone(0, "GMT")); private static final XmlSerializer serializeTester; static { serializeTester = Xml.newSerializer(); try { serializeTester.setOutput(new NullOutputStream(), "UTF-8"); } catch (IllegalArgumentException e) { AppLog.e(AppLog.T.EDITOR, "IllegalArgumentException setting test serializer output stream", e ); } catch (IllegalStateException e) { AppLog.e(AppLog.T.EDITOR, "IllegalStateException setting test serializer output stream", e ); } catch (IOException e) { AppLog.e(AppLog.T.EDITOR, "IOException setting test serializer output stream", e ); } } @SuppressWarnings("unchecked") static void serialize(XmlSerializer serializer, Object object) throws IOException { // check for scalar types: if (object instanceof Integer || object instanceof Short || object instanceof Byte) { serializer.startTag(null, TYPE_I4).text(object.toString()).endTag(null, TYPE_I4); } else if (object instanceof Long) { // Note Long should be represented by a TYPE_I8 but the WordPress end point doesn't support <i8> tag // Long usually represents IDs, so we convert them to string serializer.startTag(null, TYPE_STRING).text(object.toString()).endTag(null, TYPE_STRING); AppLog.w(T.API, "long type could be misinterpreted when sent to the WordPress XMLRPC end point"); } else if (object instanceof Double || object instanceof Float) { serializer.startTag(null, TYPE_DOUBLE).text(object.toString()).endTag(null, TYPE_DOUBLE); } else if (object instanceof Boolean) { Boolean bool = (Boolean) object; String boolStr = bool.booleanValue() ? "1" : "0"; serializer.startTag(null, TYPE_BOOLEAN).text(boolStr).endTag(null, TYPE_BOOLEAN); } else if (object instanceof String) { serializer.startTag(null, TYPE_STRING).text(makeValidInputString((String) object)).endTag(null, TYPE_STRING); } else if (object instanceof Date || object instanceof Calendar) { Date date = (Date) object; SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss", Locale.US); dateFormat.setCalendar(cal); String sDate = dateFormat.format(date); serializer.startTag(null, TYPE_DATE_TIME_ISO8601).text(sDate).endTag(null, TYPE_DATE_TIME_ISO8601); } else if (object instanceof byte[] ){ String value; try { value = Base64.encodeToString((byte[])object, Base64.DEFAULT); serializer.startTag(null, TYPE_BASE64).text(value).endTag(null, TYPE_BASE64); } catch (OutOfMemoryError e) { throw new IOException("Out of memory"); } } else if( object instanceof MediaFile ) { //convert media file binary to base64 serializer.startTag( null, "base64" ); MediaFile mediaFile = (MediaFile) object; InputStream inStream = new DataInputStream(new FileInputStream(mediaFile.getFilePath())); byte[] buffer = new byte[3600];//you must use a 24bit multiple int length = -1; String chunk = null; while ((length = inStream.read(buffer)) > 0) { chunk = Base64.encodeToString(buffer, 0, length, Base64.DEFAULT); serializer.text(chunk); } inStream.close(); serializer.endTag(null, "base64"); }else if (object instanceof List<?>) { serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); List<Object> list = (List<Object>) object; Iterator<Object> iter = list.iterator(); while (iter.hasNext()) { Object o = iter.next(); serializer.startTag(null, TAG_VALUE); serialize(serializer, o); serializer.endTag(null, TAG_VALUE); } serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY); } else if (object instanceof Object[]) { serializer.startTag(null, TYPE_ARRAY).startTag(null, TAG_DATA); Object[] objects = (Object[]) object; for (int i=0; i<objects.length; i++) { Object o = objects[i]; serializer.startTag(null, TAG_VALUE); serialize(serializer, o); serializer.endTag(null, TAG_VALUE); } serializer.endTag(null, TAG_DATA).endTag(null, TYPE_ARRAY); } else if (object instanceof Map) { serializer.startTag(null, TYPE_STRUCT); Map<String, Object> map = (Map<String, Object>) object; Iterator<Entry<String, Object>> iter = map.entrySet().iterator(); while (iter.hasNext()) { Entry<String, Object> entry = iter.next(); String key = entry.getKey(); Object value = entry.getValue(); serializer.startTag(null, TAG_MEMBER); serializer.startTag(null, TAG_NAME).text(key).endTag(null, TAG_NAME); serializer.startTag(null, TAG_VALUE); serialize(serializer, value); serializer.endTag(null, TAG_VALUE); serializer.endTag(null, TAG_MEMBER); } serializer.endTag(null, TYPE_STRUCT); } else { throw new IOException("Cannot serialize " + object); } } private static final String makeValidInputString(final String input) throws IOException { if (TextUtils.isEmpty(input)) return ""; if (serializeTester == null) return input; try { // try to encode the string as-is, 99.9% of the time it's OK serializeTester.text(input); return input; } catch (IllegalArgumentException e) { // There are characters outside the XML unicode charset as specified by the XML 1.0 standard // See http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char AppLog.e(AppLog.T.EDITOR, "There are characters outside the XML unicode charset as specified by the XML 1.0 standard", e ); } // We need to do the following things: // 1. Replace surrogates with HTML Entity. // 2. Replace emoji with their textual versions (if available on WP) // 3. Try to serialize the resulting string. // 4. If it fails again, strip characters that are not allowed in XML 1.0 final String noEmojiString = StringUtils.replaceUnicodeSurrogateBlocksWithHTMLEntities(input); try { serializeTester.text(noEmojiString); return noEmojiString; } catch (IllegalArgumentException e) { AppLog.e(AppLog.T.EDITOR, "noEmojiString still contains characters outside the XML unicode charset as specified by the XML 1.0 standard", e ); return StringUtils.stripNonValidXMLCharacters(noEmojiString); } } static Object deserialize(XmlPullParser parser) throws XmlPullParserException, IOException, NumberFormatException { parser.require(XmlPullParser.START_TAG, null, TAG_VALUE); parser.nextTag(); String typeNodeName = parser.getName(); Object obj; if (typeNodeName.equals(TYPE_INT) || typeNodeName.equals(TYPE_I4)) { String value = parser.nextText(); try { obj = Integer.parseInt(value); } catch (NumberFormatException nfe) { AppLog.w(T.API, "Server replied with an invalid 4 bytes int value, trying to parse it as 8 bytes long"); obj = Long.parseLong(value); } } else if (typeNodeName.equals(TYPE_I8)) { String value = parser.nextText(); obj = Long.parseLong(value); } else if (typeNodeName.equals(TYPE_DOUBLE)) { String value = parser.nextText(); obj = Double.parseDouble(value); } else if (typeNodeName.equals(TYPE_BOOLEAN)) { String value = parser.nextText(); obj = value.equals("1") ? Boolean.TRUE : Boolean.FALSE; } else if (typeNodeName.equals(TYPE_STRING)) { obj = parser.nextText(); } else if (typeNodeName.equals(TYPE_DATE_TIME_ISO8601)) { dateFormat.setCalendar(cal); String value = parser.nextText(); try { obj = dateFormat.parseObject(value); } catch (ParseException e) { AppLog.e(T.API, e); obj = value; } } else if (typeNodeName.equals(TYPE_BASE64)) { String value = parser.nextText(); BufferedReader reader = new BufferedReader(new StringReader(value)); String line; StringBuffer sb = new StringBuffer(); while ((line = reader.readLine()) != null) { sb.append(line); } obj = Base64.decode(sb.toString(), Base64.DEFAULT); } else if (typeNodeName.equals(TYPE_ARRAY)) { parser.nextTag(); // TAG_DATA (<data>) parser.require(XmlPullParser.START_TAG, null, TAG_DATA); parser.nextTag(); List<Object> list = new ArrayList<Object>(); while (parser.getName().equals(TAG_VALUE)) { list.add(deserialize(parser)); parser.nextTag(); } parser.require(XmlPullParser.END_TAG, null, TAG_DATA); parser.nextTag(); // TAG_ARRAY (</array>) parser.require(XmlPullParser.END_TAG, null, TYPE_ARRAY); obj = list.toArray(); } else if (typeNodeName.equals(TYPE_STRUCT)) { parser.nextTag(); Map<String, Object> map = new HashMap<String, Object>(); while (parser.getName().equals(TAG_MEMBER)) { String memberName = null; Object memberValue = null; while (true) { parser.nextTag(); String name = parser.getName(); if (name.equals(TAG_NAME)) { memberName = parser.nextText(); } else if (name.equals(TAG_VALUE)) { memberValue = deserialize(parser); } else { break; } } if (memberName != null && memberValue != null) { map.put(memberName, memberValue); } parser.require(XmlPullParser.END_TAG, null, TAG_MEMBER); parser.nextTag(); } parser.require(XmlPullParser.END_TAG, null, TYPE_STRUCT); obj = map; } else { throw new IOException("Cannot deserialize " + parser.getName()); } parser.nextTag(); // TAG_VALUE (</value>) parser.require(XmlPullParser.END_TAG, null, TAG_VALUE); return obj; } }