Facebook SDK for Unity PList Fix

Hey everyone!

Just wanted to share this quick fix I did in case it was troubling anyone else. Facebook SDK blindly adds items to the PList file without checking that they exist. Tisk tisk…

So I added XML checks to see if the nodes already exist. The two checks I added were for “FacebookAppID”, to avoid redundancies, and “CFBundleURLTypes”, which is necessary for any app using custom URL Schemes. Facebook adds theirs below yours, effectively overwriting your scheme entirely.

Here’s the Code, from “Plist.Mod.cs”.

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Text;

namespace UnityEditor.FacebookEditor
{
    public class PlistMod
    {
        private static XmlNode FindPlistDictNode(XmlDocument doc)
        {
            XmlNode curr = doc.FirstChild;
            while(curr != null)
            {
                if(curr.Name.Equals("plist")  curr.ChildNodes.Count == 1)
                {
                    XmlNode dict = curr.FirstChild;
                    if(dict.Name.Equals("dict"))
                        return dict;
                }
                curr = curr.NextSibling;
            }
            return null;
        }
        
        private static XmlElement AddChildElement(XmlDocument doc, XmlNode parent, string elementName, string innerText=null)
        {
            XmlElement newElement = doc.CreateElement(elementName);
            if(innerText != null  innerText.Length > 0)
                newElement.InnerText = innerText;
            
            parent.AppendChild(newElement);
            return newElement;
        }
        
        public static void UpdatePlist(string path, string appId)
        {
            string fileName = "Info.plist";
            string fullPath = Path.Combine(path, fileName);
            
            if(appId == null || appId.Length <= 0 || appId.Equals("0"))
            {
                Debug.LogError("You didn't specify a Facebook app ID.  Please add one using the Facebook menu in the main Unity editor.");
                return;
            }
            
            XmlDocument doc = new XmlDocument();
            doc.Load(fullPath);
            
            if(doc == null)
            {
                Debug.LogError("Couldn't load " + fullPath);
                return;
            }
            
            XmlNode dict = FindPlistDictNode(doc);
            if(dict == null)
            {
                Debug.LogError("Error parsing " + fullPath);
                return;
            }
            
            //add the app id to the plist
            //the xml should end up looking like this
            /*
            <key>FacebookAppID</key>
            <string>YOUR_APP_ID</string>
             */
			
			//add facebookappid if it's missing
			XmlNode fbAppID = doc.SelectSingleNode( "/plist/dict/key[text() = 'FacebookAppID']" );
			if (fbAppID == null)
			{
	            AddChildElement(doc, dict, "key", "FacebookAppID");
	            AddChildElement(doc, dict, "string", appId);
			}
            
            //here's how the custom url scheme should end up looking
            /*
             <key>CFBundleURLTypes</key>
             <array>
                 <dict>
                     <key>CFBundleURLSchemes</key>
                     <array>
                         <string>fbYOUR_APP_ID</string>
                     </array>
                 </dict>
             </array>
            */
			
			//first attempt to find key by text
			XmlNode urlTypes = doc.SelectSingleNode( "/plist/dict/key[text() = 'CFBundleURLTypes']" );
			XmlElement urlSchemeTop = null;
			//if key missing, add it
			if (urlTypes == null)
			{
           		AddChildElement(doc, dict, "key", "CFBundleURLTypes");
				urlSchemeTop = AddChildElement(doc, dict, "array");
			}
			else
				urlSchemeTop = urlTypes.NextSibling as XmlElement;
			
			//add facebook url dict
            XmlElement urlSchemeDict = AddChildElement(doc, urlSchemeTop, "dict");
            AddChildElement(doc, urlSchemeDict, "key", "CFBundleURLSchemes");
            XmlElement innerArray = AddChildElement(doc, urlSchemeDict, "array");
            AddChildElement(doc, innerArray, "string", "fb" + appId);
            
            
            doc.Save(fullPath);
            
            //the xml writer barfs writing out part of the plist header.
            //so we replace the part that it wrote incorrectly here
            System.IO.StreamReader reader = new System.IO.StreamReader(fullPath);
            string textPlist = reader.ReadToEnd();
            reader.Close();
            
            int fixupStart = textPlist.IndexOf("<!DOCTYPE plist PUBLIC");
            if(fixupStart <= 0)
                return;
            int fixupEnd = textPlist.IndexOf('>', fixupStart);
            if(fixupEnd <= 0)
                return;
            
            string fixedPlist = textPlist.Substring(0, fixupStart);
            fixedPlist += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">";
            fixedPlist += textPlist.Substring(fixupEnd+1);
            
            System.IO.StreamWriter writer = new System.IO.StreamWriter(fullPath, false);
            writer.Write(fixedPlist);
            writer.Close();
        }
    }
}

If requested, I can upload a diff so you can see exactly what I changed. Hope this helps someone else out there!

1 Like

Thank you, good job! Sometimes I have also problems with Info.plist created by Facebook sdk:

It can be checked by plutil utility
$ plutil Info.plist
Info.plist: Close tag on line 45 does not match open tag dict

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Text;
using System.Text.RegularExpressions;

namespace UnityEditor.FacebookEditor
{
	public class PlistMod
	{
		private static XmlNode FindPlistDictNode(XmlDocument doc)
		{
			XmlNode curr = doc.FirstChild;
			while(curr != null)
			{
				if(curr.Name.Equals("plist")  curr.ChildNodes.Count == 1)
				{
					XmlNode dict = curr.FirstChild;
					if(dict.Name.Equals("dict"))
						return dict;
				}
				curr = curr.NextSibling;
			}
			return null;
		}
		
		private static XmlElement AddChildElement(XmlDocument doc, XmlNode parent, string elementName, string innerText=null)
		{
			XmlElement newElement = doc.CreateElement(elementName);
			if(innerText != null  innerText.Length > 0)
				newElement.InnerText = innerText;
			
			parent.AppendChild(newElement);
			return newElement;
		}
		
		public static void UpdatePlist(string path, string appId)
		{
			string fileName = "Info.plist";
			string fullPath = Path.Combine(path, fileName);
			
			if(appId == null || appId.Length <= 0 || appId.Equals("0"))
			{
				Debug.LogError("You didn't specify a Facebook app ID.  Please add one using the Facebook menu in the main Unity editor.");
				return;
			}
			
			XmlDocument doc = new XmlDocument();
			doc.Load(fullPath);
			doc.PreserveWhitespace = true;
			
			if(doc == null)
			{
				Debug.LogError("Couldn't load " + fullPath);
				return;
			}
			
			XmlNode dict = FindPlistDictNode(doc);
			if(dict == null)
			{
				Debug.LogError("Error parsing " + fullPath);
				return;
			}
			
			//add the app id to the plist
			//the xml should end up looking like this
			/*
            <key>FacebookAppID</key>
            <string>YOUR_APP_ID</string>
             */
			
			//add facebookappid if it's missing
			XmlNode fbAppID = doc.SelectSingleNode( "/plist/dict/key[text() = 'FacebookAppID']" );
			if (fbAppID == null)
			{
				AddChildElement(doc, dict, "key", "FacebookAppID");
				AddChildElement(doc, dict, "string", appId);
			}
			
			//here's how the custom url scheme should end up looking
			/*
             <key>CFBundleURLTypes</key>
             <array>
                 <dict>
                     <key>CFBundleURLSchemes</key>
                     <array>
                         <string>fbYOUR_APP_ID</string>
                     </array>
                 </dict>
             </array>
            */
			
			//first attempt to find key by text
			XmlNode urlTypes = doc.SelectSingleNode( "/plist/dict/key[text() = 'CFBundleURLTypes']" );
			XmlElement urlSchemeTop = null;
			//if key missing, add it
			if (urlTypes == null) {
				AddChildElement(doc, dict, "key", "CFBundleURLTypes");
				urlSchemeTop = AddChildElement(doc, dict, "array");
			} else {
				urlSchemeTop = urlTypes.NextSibling as XmlElement;
			}

			XmlElement innerArray = null;
			//search for the facebook url scheme
			foreach (XmlNode n in urlSchemeTop.ChildNodes) {
				if (n["key"].InnerText == "CFBundleURLSchemes") {
					innerArray = n["array"];
					break;
				}
			}

			if (innerArray == null) {
				//add facebook url dict
				XmlElement urlSchemeDict = AddChildElement(doc, urlSchemeTop, "dict");
				AddChildElement(doc, urlSchemeDict, "key", "CFBundleURLSchemes");
				innerArray = AddChildElement(doc, urlSchemeDict, "array");
			}

			bool hasAppScheme = false;
			string handlerString = "fb" + appId;
			foreach (XmlNode n in innerArray.ChildNodes) {
				if (n.Name == "string") {
					if (n.InnerText == handlerString) {
						hasAppScheme = true;
						break;
					}
				}
			}

			if (!hasAppScheme) {
				AddChildElement(doc, innerArray, "string", handlerString);
			}

			//Strip whitespace from empty strings
			Regex nonwhite = new Regex("\\S");
			XmlNodeList elemList = doc.GetElementsByTagName("string");
			for (int i=0; i < elemList.Count; i++) {   
				if (!nonwhite.IsMatch(elemList[i].InnerText)) {
					elemList[i].InnerText = "";
				}
			}

			XmlWriterSettings settings = new XmlWriterSettings {
				Indent = true,
				IndentChars = "\t",
				NewLineHandling = NewLineHandling.None
			};
			XmlWriter xmlwriter = XmlWriter.Create(fullPath, settings);
			doc.Save(xmlwriter);
			xmlwriter.Close();
			
			//the xml writer barfs writing out part of the plist header.
			//so we replace the part that it wrote incorrectly here
			System.IO.StreamReader reader = new System.IO.StreamReader(fullPath);
			string textPlist = reader.ReadToEnd();
			reader.Close();

			//strip extra indentation (not really necessary)
			textPlist = (new Regex("^\\t",RegexOptions.Multiline)).Replace(textPlist,"");

			//strip whitespace from booleans (not really necessary)
			textPlist = (new Regex("<(true|false) />",RegexOptions.IgnoreCase)).Replace(textPlist,"<$1/>");

			int fixupStart = textPlist.IndexOf("<!DOCTYPE plist PUBLIC");
			if(fixupStart <= 0)
				return;
			int fixupEnd = textPlist.IndexOf('>', fixupStart);
			if(fixupEnd <= 0)
				return;
			
			string fixedPlist = textPlist.Substring(0, fixupStart);
			fixedPlist += "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">";
			fixedPlist += textPlist.Substring(fixupEnd+1);

			System.IO.StreamWriter writer = new System.IO.StreamWriter(fullPath, false);
			writer.Write(fixedPlist);
			writer.Close();
		}
	}
}

Also prevents multiple URL schemes from being added.

1 Like

Thanks for posting these! I think the forum might be removing your && symbols, such as in the AddChildElement method’s if statement.

I found a simpler fix for the duplicate /string issue. Add this to PlistMod.cs. By adding a space to the empty string, system.XmlDocument stops splitting the line, and the next pass of an append build stops blowing up:
//Strip whitespace from empty strings
Regex nonwhite = new Regex(“\S”);
XmlNodeList elemList = doc.GetElementsByTagName(“string”);
for (int i = 0; i < elemList.Count; i++)
{
if (!nonwhite.IsMatch(elemList*.InnerText))*
{
// Replace with something that keeps formatter from splitting the line.
elemList*.InnerText = " ";*
}
}
doc.Save(fullPath);